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

120 files changed:
apps/workbench/app/assets/javascripts/application.js
apps/workbench/app/assets/javascripts/editable.js
apps/workbench/app/assets/javascripts/filterable.js [new file with mode: 0644]
apps/workbench/app/assets/javascripts/folders.js [deleted file]
apps/workbench/app/assets/javascripts/infinite_scroll.js [new file with mode: 0644]
apps/workbench/app/assets/javascripts/pipeline_instances.js
apps/workbench/app/assets/javascripts/select_modal.js
apps/workbench/app/assets/javascripts/selection.js
apps/workbench/app/assets/stylesheets/application.css.scss
apps/workbench/app/assets/stylesheets/folders.css.scss [deleted file]
apps/workbench/app/assets/stylesheets/jobs.css.scss
apps/workbench/app/assets/stylesheets/projects.css.scss [new file with mode: 0644]
apps/workbench/app/assets/stylesheets/select_modal.css.scss
apps/workbench/app/controllers/actions_controller.rb
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/folders_controller.rb [deleted file]
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/controllers/pipeline_templates_controller.rb
apps/workbench/app/controllers/projects_controller.rb [new file with mode: 0644]
apps/workbench/app/controllers/users_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/helpers/folders_helper.rb [deleted file]
apps/workbench/app/helpers/pipeline_instances_helper.rb
apps/workbench/app/helpers/projects_helper.rb [new file with mode: 0644]
apps/workbench/app/models/arvados_base.rb
apps/workbench/app/models/collection.rb
apps/workbench/app/models/group.rb
apps/workbench/app/models/human.rb
apps/workbench/app/models/job.rb
apps/workbench/app/models/link.rb
apps/workbench/app/models/pipeline_instance.rb
apps/workbench/app/models/pipeline_template.rb
apps/workbench/app/models/specimen.rb
apps/workbench/app/models/trait.rb
apps/workbench/app/views/application/_arvados_attr_value.html.erb
apps/workbench/app/views/application/_choose.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_choose.js.erb [moved from apps/workbench/app/views/folders/_choose.js.erb with 88% similarity]
apps/workbench/app/views/application/_content.html.erb
apps/workbench/app/views/application/_content_layout.html.erb
apps/workbench/app/views/application/_delete_object_button.html.erb
apps/workbench/app/views/application/_selection_checkbox.html.erb
apps/workbench/app/views/application/_show_advanced.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_advanced_api_response.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_advanced_cli_example.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_advanced_curl_example.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_advanced_metadata.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_advanced_python_example.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_json.html.erb [deleted file]
apps/workbench/app/views/application/_show_metadata.html.erb [deleted file]
apps/workbench/app/views/application/_show_object_button.html.erb
apps/workbench/app/views/application/_show_object_description_cell.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_recent.html.erb
apps/workbench/app/views/application/_svg_div.html.erb
apps/workbench/app/views/application/index.html.erb
apps/workbench/app/views/application/show.html.erb
apps/workbench/app/views/collections/_choose.js.erb [new symlink]
apps/workbench/app/views/collections/_choose_rows.html.erb [new file with mode: 0644]
apps/workbench/app/views/collections/_index_tbody.html.erb
apps/workbench/app/views/collections/_show_files.html.erb
apps/workbench/app/views/collections/_show_object_description_cell.html.erb [new file with mode: 0644]
apps/workbench/app/views/collections/_show_recent.html.erb
apps/workbench/app/views/collections/show.html.erb
apps/workbench/app/views/folders/_show_folders.html.erb [deleted file]
apps/workbench/app/views/folders/show.html.erb [deleted file]
apps/workbench/app/views/jobs/_show_details.html.erb [new file with mode: 0644]
apps/workbench/app/views/jobs/_show_object_description_cell.html.erb [new file with mode: 0644]
apps/workbench/app/views/layouts/application.html.erb
apps/workbench/app/views/pipeline_instances/_component_labels.html.erb [new file with mode: 0644]
apps/workbench/app/views/pipeline_instances/_show_compare.html.erb
apps/workbench/app/views/pipeline_instances/_show_components.html.erb
apps/workbench/app/views/pipeline_instances/_show_inputs.html.erb
apps/workbench/app/views/pipeline_instances/_show_object_description_cell.html.erb [new file with mode: 0644]
apps/workbench/app/views/pipeline_instances/_show_recent.html.erb
apps/workbench/app/views/pipeline_instances/compare.html.erb
apps/workbench/app/views/pipeline_templates/_choose.js.erb [new symlink]
apps/workbench/app/views/pipeline_templates/_choose_rows.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/_choose.html.erb [moved from apps/workbench/app/views/folders/_choose.html.erb with 52% similarity]
apps/workbench/app/views/projects/_choose.js.erb [new symlink]
apps/workbench/app/views/projects/_index_jobs_and_pipelines.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/_index_projects.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/_show_contents.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/_show_contents_rows.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/_show_featured.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/_show_permissions.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/index.html.erb [new file with mode: 0644]
apps/workbench/app/views/projects/remove_items.js.erb [moved from apps/workbench/app/views/folders/remove_item.js.erb with 100% similarity]
apps/workbench/app/views/users/_home.html.erb
apps/workbench/app/views/users/inactive.html.erb [new file with mode: 0644]
apps/workbench/app/views/users/welcome.html.erb
apps/workbench/config/routes.rb
apps/workbench/test/functional/collections_controller_test.rb
apps/workbench/test/functional/folders_controller_test.rb [deleted file]
apps/workbench/test/functional/projects_controller_test.rb [new file with mode: 0644]
apps/workbench/test/functional/users_controller_test.rb
apps/workbench/test/integration/collections_test.rb
apps/workbench/test/integration/folders_test.rb [deleted file]
apps/workbench/test/integration/logins_test.rb
apps/workbench/test/integration/pipeline_instances_test.rb
apps/workbench/test/integration/projects_test.rb [new file with mode: 0644]
apps/workbench/test/integration/smoke_test.rb
apps/workbench/test/integration/users_test.rb
apps/workbench/test/integration/virtual_machines_test.rb
apps/workbench/test/unit/arvados_resource_list_test.rb
apps/workbench/test/unit/group_test.rb
apps/workbench/test/unit/helpers/folders_helper_test.rb [deleted file]
apps/workbench/test/unit/helpers/projects_helper_test.rb [new file with mode: 0644]
services/api/db/migrate/20140607150616_rename_folder_to_project.rb_deferred [new file with mode: 0644]
services/api/test/fixtures/groups.yml
services/api/test/fixtures/links.yml
services/api/test/fixtures/pipeline_instances.yml
services/api/test/fixtures/specimens.yml
services/api/test/functional/arvados/v1/filters_test.rb
services/api/test/functional/arvados/v1/groups_controller_test.rb
services/api/test/functional/arvados/v1/links_controller_test.rb
services/api/test/unit/group_test.rb
services/api/test/unit/link_test.rb
services/api/test/unit/owner_test.rb

index 7b09d5242cb5283f971ca216827c3105f55cc731..e35e93c5d33af1f78a0d4ef6298d827b11095ffc 100644 (file)
@@ -119,6 +119,11 @@ jQuery(function($){
             $('.btn').button();
         });
 
+    $(document).
+        on('ready ajax:complete', function() {
+            $('[data-toggle~=tooltip]').tooltip({container:'body'});
+        });
+
     HeaderRowFixer = function(selector) {
         this.duplicateTheadTr = function() {
             $(selector).each(function() {
index 16bb7f6cd87e9662d0c8e0774605e4baf1e6310f..ab66833c286a6c4666c3162309fe8267be03fea9 100644 (file)
@@ -41,10 +41,9 @@ $.fn.editable.defaults.validate = function (value) {
 
 $(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
@@ -78,6 +77,15 @@ $(document).
                       });
                 }
             });
+    }).
+    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">'
diff --git a/apps/workbench/app/assets/javascripts/filterable.js b/apps/workbench/app/assets/javascripts/filterable.js
new file mode 100644 (file)
index 0000000..76c5ac3
--- /dev/null
@@ -0,0 +1,42 @@
+$(document).
+    on('paste keyup change', 'input[type=text].filterable-control', function() {
+        var q = new RegExp($(this).val(), 'i');
+        $($(this).attr('data-filterable-target')).
+            addClass('filterable-container').
+            data('q', q).
+            trigger('refresh');
+    }).on('refresh', '.filterable-container', function() {
+        var q = $(this).data('q');
+        var filters = $(this).data('filters');
+        $('.filterable', this).hide().filter(function() {
+            var $row = $(this);
+            var pass = true;
+            if (q && !$row.text().match(q))
+                return false;
+            if (filters) {
+                $.each(filters, function(filterby, val) {
+                    if (!val) return;
+                    if (!pass) return;
+                    pass = false;
+                    $.each(val.split(" "), function(i, e) {
+                        if ($row.attr(filterby) == e)
+                            pass = true;
+                    });
+                });
+            }
+            return pass;
+        }).show();
+        $('.infinite-scroller').add(window).trigger('scroll');
+    }).on('change', 'select.filterable-control', function() {
+        var val = $(this).val();
+        var filterby = $(this).attr('data-filterable-attribute');
+        var $target = $($(this).attr('data-filterable-target')).
+            addClass('filterable-container');
+        var filters = $target.data('filters') || {};
+        filters[filterby] = val;
+        $target.
+            data('filters', filters).
+            trigger('refresh');
+    }).on('ajax:complete', function() {
+        $('.filterable-control').trigger('change');
+    });
diff --git a/apps/workbench/app/assets/javascripts/folders.js b/apps/workbench/app/assets/javascripts/folders.js
deleted file mode 100644 (file)
index 10695cf..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-$(document).
-    on('ready ajax:complete', function() {
-        $("[data-toggle='x-editable']").click(function(e) {
-            e.stopPropagation();
-            $($(this).attr('data-toggle-selector')).editable('toggle');
-        });
-    }).on('paste keyup change', 'input.search-folder-contents', function() {
-        var q = new RegExp($(this).val(), 'i');
-        $(this).closest('div.panel').find('tbody tr').each(function() {
-            $(this).toggle(!!$(this).text().match(q));
-        });
-    });
diff --git a/apps/workbench/app/assets/javascripts/infinite_scroll.js b/apps/workbench/app/assets/javascripts/infinite_scroll.js
new file mode 100644 (file)
index 0000000..3e15c9d
--- /dev/null
@@ -0,0 +1,52 @@
+function maybe_load_more_content() {
+    var scroller = this;        // element with scroll bars
+    var container;              // element that receives new content
+    var src;                    // url for retrieving content
+    var scrollHeight;
+    scrollHeight = scroller.scrollHeight || $('body')[0].scrollHeight;
+    if ($(scroller).scrollTop() + $(scroller).height()
+        >
+        scrollHeight - 50) {
+        container = $(this).data('infinite-container');
+        src = $(container).attr('data-infinite-content-href');
+        if (!src)
+            // Finished
+            return;
+        // Don't start another request until this one finishes
+        $(container).attr('data-infinite-content-href', null);
+        $.ajax(src,
+               {dataType: 'json',
+                type: 'GET',
+                data: {},
+                context: {container: container, src: src}}).
+            fail(function(jqxhr, status, error) {
+                if (jqxhr.readyState == 0 || jqxhr.status == 0) {
+                    message = "Cancelled."
+                } else if (jqxhr.responseJSON && jqxhr.responseJSON.errors) {
+                    message = jqxhr.responseJSON.errors.join("; ");
+                } else {
+                    message = "Request failed.";
+                }
+                // TODO: report this to the user.
+                console.log(message);
+                $(this.container).attr('data-infinite-content-href', this.src);
+            }).
+            done(function(data, status, jqxhr) {
+                $(this.container).append(data.content);
+                $(this.container).attr('data-infinite-content-href', data.next_page_href);
+                $(document).trigger('ajax:complete');
+            });
+    }
+}
+$(document).
+    on('ready ajax:complete', function() {
+        $('[data-infinite-scroller]').each(function() {
+            var $scroller = $($(this).attr('data-infinite-scroller'));
+            if (!$scroller.hasClass('smart-scroll'))
+                $scroller = $(window);
+            $scroller.
+                addClass('infinite-scroller').
+                data('infinite-container', this).
+                on('scroll', maybe_load_more_content);
+        });
+    });
index c61e336c7ae2377c343fa159b6c9c39abf4fc33d..f206213ed2e97be81acf97b4f12b1cb059d02227 100644 (file)
@@ -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");
     }
@@ -48,18 +48,41 @@ $(document).on('ready ajax:complete', function() {
 });
 
 $(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 85d97c998222a5b9449884b2653d26b59bd1fdd2..6e22d8013924661a50ebcf39a6123dad5eb673b0 100644 (file)
@@ -1,4 +1,5 @@
 $(document).on('click', '.selectable', function() {
+    var any;
     var $this = $(this);
     if (!$this.hasClass('multiple')) {
         $this.closest('.selectable-container').
@@ -6,20 +7,33 @@ $(document).on('click', '.selectable', function() {
             removeClass('active');
     }
     $this.toggleClass('active');
+    any = ($this.
+           closest('.selectable-container').
+           find('.selectable.active').length > 0)
+    $this.
+        closest('.modal').
+        find('[data-enable-if-selection]').
+        prop('disabled', !any);
 }).on('click', '.modal button[data-action-href]', function() {
     var selection = [];
-    var data = {};
+    var data = [];
     var $modal = $(this).closest('.modal');
+    var action_data = $(this).data('action-data');
+    var selection_param = action_data.selection_param;
     $modal.find('.modal-error').removeClass('hide').hide();
     $modal.find('.selectable.active[data-object-uuid]').each(function() {
-        selection.push($(this).attr('data-object-uuid'));
+        var val = $(this).attr('data-object-uuid');
+        data.push({name: selection_param, value: val});
+    });
+    $.each(action_data, function(key, value) {
+        data.push({name: key, value: value});
     });
-    data[$(this).data('action-data').selection_param] = selection[0];
     $.ajax($(this).attr('data-action-href'),
            {dataType: 'json',
             type: $(this).attr('data-method'),
             data: data,
-            context: {modal: $modal}}).
+            traditional: true,
+            context: {modal: $modal, action_data: action_data}}).
         fail(function(jqxhr, status, error) {
             if (jqxhr.readyState == 0 || jqxhr.status == 0) {
                 message = "Cancelled."
@@ -32,8 +46,15 @@ $(document).on('click', '.selectable', function() {
                 html('<div class="alert alert-danger">' + message + '</div>').
                 show();
         }).
-        success(function() {
+        done(function(data, status, jqxhr) {
+            var event_name = this.action_data.success;
             this.modal.find('.modal-error').hide();
-            window.location.reload();
+            $(document).trigger(event_name!=null ? event_name : 'page-refresh',
+                                [data, status, jqxhr, this.action_data]);
         });
 });
+$(document).on('page-refresh', function(event, data, status, jqxhr, action_data) {
+    window.location.reload();
+}).on('redirect-to-created-object', function(event, data, status, jqxhr, action_data) {
+    window.location.href = data.href.replace(/^[^\/]*\/\/[^\/]*/, '');
+});
index 1e32c635642ef3fd9f109df931aebc23f8a8013b..84d65cda2d84419c20d5bf6b00a96773aaae9680 100644 (file)
@@ -58,7 +58,7 @@ jQuery(function($){
         if (lst.length > 0) {
             html = '<li><a href="#" class="btn btn-xs btn-info" id="clear_selections_button"><i class="fa fa-fw fa-ban"></i> Clear selections</a></li>';
             if (this_object_uuid.match('-j7d0g-'))
-                html += '<li><button class="btn btn-xs btn-info" type="submit" name="copy_selections_into_folder" id="copy_selections_into_folder"><i class="fa fa-fw fa-folder-open"></i> Copy selections into this folder</button></li>';
+                html += '<li><button class="btn btn-xs btn-info" type="submit" name="copy_selections_into_project" id="copy_selections_into_project"><i class="fa fa-fw fa-folder-open"></i> Copy selections into this project</button></li>';
             html += '<li><button class="btn btn-xs btn-info" type="submit" name="combine_selected_files_into_collection" '
                 + ' id="combine_selected_files_into_collection">'
                 + '<i class="fa fa-fw fa-archive"></i> Combine selected collections and files into a new collection</button></li>'
@@ -100,13 +100,11 @@ jQuery(function($){
 
         $('.remove-selection').on('click', remove_selection_click);
         $('#clear_selections_button').on('click', clear_selections);
+        $(document).trigger('selections-updated', [lst]);
     };
 
     $(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'));
@@ -116,7 +114,6 @@ jQuery(function($){
             }
         });
 
-
     $(window).on('load storage', update_count);
 
     $('#selection-form-content').on("click", function(e) {
@@ -178,3 +175,42 @@ select_form_sources = null;
         return ret;
     };
 })();
+
+function dispatch_selection_action() {
+    // Build a new "href" attribute for this link by starting with the
+    // "data-href" attribute and appending ?foo[]=bar&foo[]=baz (or
+    // &foo=... as appropriate) to reflect the current object
+    // selections.
+    var data = [];
+    var param_name = $(this).attr('data-selection-param-name');
+    var href = $(this).attr('data-href');
+    $('.persistent-selection:checkbox:checked').each(function() {
+        data.push({name: param_name, value: $(this).val()});
+    });
+    if (href.indexOf('?') >= 0)
+        href += '&';
+    else
+        href += '?';
+    href += $.param(data, true);
+    $(this).attr('href', href);
+    return true;
+}
+
+function enable_disable_selection_actions() {
+    var $checked = $('.persistent-selection:checkbox:checked');
+    $('[data-selection-action]').
+        closest('div.btn-group-sm').
+        find('*').
+        prop('disabled', ($checked.length == 0));
+    $('[data-selection-action=compare]').
+        closest('li').
+        toggleClass('disabled',
+                    ($checked.filter('[value*=-d1hrv-]').length < 2) ||
+                    ($checked.not('[value*=-d1hrv-]').length > 0));
+}
+
+$(document).
+    on('selections-updated ready ajax:complete', function() {
+        $('[data-selection-action]').click(dispatch_selection_action);
+        enable_disable_selection_actions();
+    });
index 64c50985c85e885a2059d9c7bd3c41806be4e105..c01195c1d84dc345eef2d5e5bb29628feccce53f 100644 (file)
@@ -47,6 +47,7 @@ table.table-justforlayout {
     font-size: .8em;
     color: #888;
 }
+.arvados-filename,
 .arvados-uuid {
     font-size: .8em;
     font-family: monospace;
@@ -64,6 +65,9 @@ table.arv-index tbody td.arv-object-AuthorizedKey.arv-attr-public_key {
     overflow-x: hidden;
     max-width: 120px;
 }
+table.arv-index > thead > tr > th {
+    border-top: none;
+}
 table.table-fixedlayout {
     white-space: nowrap;
     table-layout: fixed;
@@ -86,13 +90,40 @@ form.small-form-margin {
     white-space: nowrap;
 }
 
-.navbar .nav li.nav-separator span {
-    display: block;
-    float: none;
-    color: #bbbbbb;
-    padding: 10px 0 10px;
-    text-decoration: none;
-    text-shadow: 0 1px 0 #ffffff;
+/* top nav */
+$top-nav-bg: #3c163d;
+$top-nav-bg-bottom: #260027;
+nav.navbar-fixed-top .navbar-brand {
+    color: #79537a;
+    letter-spacing: 0.4em;
+}
+nav.navbar-fixed-top {
+    background: $top-nav-bg;
+    background: linear-gradient(to bottom, $top-nav-bg 0%,$top-nav-bg-bottom 100%);
+}
+.navbar.breadcrumbs {
+    line-height: 50px;
+    border-radius: 0;
+    margin-bottom: 0;
+    border-right: 0;
+    border-left: 0;
+}
+.navbar.breadcrumbs .nav > li > a,
+.navbar.breadcrumbs .nav > li {
+    color: #000;
+}
+.navbar.breadcrumbs .nav > li.nav-separator > i {
+    color: #bbb;
+}
+nav.navbar-fixed-top .navbar-nav.navbar-right > li.open > a,
+nav.navbar-fixed-top .navbar-nav.navbar-right > li.open > a:focus,
+nav.navbar-fixed-top .navbar-nav.navbar-right > li.open > a:hover {
+    background: lighten($top-nav-bg, 5%);
+}
+nav.navbar-fixed-top .navbar-nav.navbar-right > li > a,
+nav.navbar-fixed-top .navbar-nav.navbar-right > li > a:focus,
+nav.navbar-fixed-top .navbar-nav.navbar-right > li > a:hover {
+    color: #fff;
 }
 
 .dax {
@@ -171,26 +202,26 @@ table.table-fixed-header-row tbody {
     z-index:1055;
 }
 
-.navbar-nav.side-nav {
-    box-shadow: inset -1px 0 #e7e7e7;
+/* Do not leave space for left-nav */
+div#wrapper {
+  padding-left: 0;
 }
-.navbar-nav.side-nav > li:first-child {
-    margin-top: 5px; /* keep "hover" bg below top nav bottom border */
+
+.arv-description-as-subtitle {
+  padding-bottom: 1em;
 }
-.navbar-nav.side-nav > li > a {
-    padding-top: 10px;
-    padding-bottom: 10px;
+.arv-description-in-table {
+  max-height: 3.5em;
+  overflow-x: hidden;
+  overflow-y: hidden;
 }
-.navbar-nav.side-nav > li.dropdown > ul.dropdown-menu > li > a {
-    padding-top: 5px;
-    padding-bottom: 5px;
+.arv-description-in-table:hover {
+  overflow-y: auto;
 }
-.navbar-nav.side-nav a.active,
-.navbar-nav.side-nav a:hover,
-.navbar-nav.side-nav a:focus {
-    border-right: 1px solid #ffffff;
-    background: #ffffff;
+
+.btn.btn-nodecorate {
+  border: none;
 }
 svg text {
     font-size: 6pt;
-}
\ No newline at end of file
+}
diff --git a/apps/workbench/app/assets/stylesheets/folders.css.scss b/apps/workbench/app/assets/stylesheets/folders.css.scss
deleted file mode 100644 (file)
index a033e87..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-.arv-folder-list > .row {
-    padding-top: 5px;
-    padding-bottom: 5px;
-    padding-right: 1em;
-}
-.arv-folder-list > .row.folder:hover {
-    background: #d9edf7;
-}
-.arv-folder-list > .row.folder.active,
-.arv-folder-list > .row.folder.active:hover {
-    background: #428bca;
-    color: #fff;
-}
index e485745b3faf04b26888b89de6d668c9ee63436f..14f86997f43f9ec46fcbc21561c4a70d91bcdd86 100644 (file)
@@ -1,3 +1,10 @@
-// Place all the styles related to the Jobs controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
+.arv-job-log-window {
+    height: 20em;
+    white-space: nowrap;
+    overflow: scroll;
+    background: black;
+    color: white;
+    font-family: monospace;
+    font-size: .8em;
+    border: 2px solid black;
+}
diff --git a/apps/workbench/app/assets/stylesheets/projects.css.scss b/apps/workbench/app/assets/stylesheets/projects.css.scss
new file mode 100644 (file)
index 0000000..53b352d
--- /dev/null
@@ -0,0 +1,12 @@
+.arv-project-list > .row {
+    padding-top: 5px;
+    padding-bottom: 5px;
+    padding-right: 1em;
+}
+.arv-project-list > .row.project:hover {
+    background: #d9edf7;
+}
+div.scroll-20em {
+    height: 20em;
+    overflow-y: scroll;
+}
index 6fe56515aaf892e3d41eb0db1d4787f2411e4447..5aab6698259afb271c0f1feac0782b58df3382ef 100644 (file)
@@ -1,3 +1,15 @@
+.selectable-container > .row {
+    padding-top: 5px;
+    padding-bottom: 5px;
+    padding-right: 1em;
+}
 .selectable.active, .selectable:hover {
     background: #d9edf7;
 }
+.selectable.active,
+.selectable.active *,
+.selectable.active:hover,
+.selectable.active:hover * {
+    background: #428bca;
+    color: #fff;
+}
index 368d9a8e8ce3a9b106b50d1b0f385603ea993021..06775fc924521bdfa6a7796098bbed1cabaaa8c0 100644 (file)
@@ -19,16 +19,45 @@ class ActionsController < ApplicationController
     redirect_to :back
   end
 
-  expose_action :copy_selections_into_folder do
+  expose_action :copy_selections_into_project do
+    link_selections = Link.filter([['uuid','in',params["selection"]]])
+    link_uuids = link_selections.collect(&:uuid)
+
+    # Given a link uuid, we'll add the link's head_uuid. Given another
+    # type, we'll add the object itself.
+    uuids_to_add = params["selection"] - link_uuids
+    uuids_to_add += link_selections.collect(&:head_uuid)
+
+    # Skip anything that's already here.
     already_named = Link.
       filter([['tail_uuid','=',@object.uuid],
-              ['head_uuid','in',params["selection"]]]).
+              ['head_uuid','in',uuids_to_add],
+              ['link_class','=','name']]).
       collect(&:head_uuid)
-    (params["selection"] - already_named).each do |s|
-      Link.create(tail_uuid: @object.uuid,
-                  head_uuid: s,
-                  link_class: 'name',
-                  name: "#{s} added #{Time.now}")
+    uuids_to_add -= already_named
+
+    # Given a name link, we'll try to add the linked object using the
+    # same name.
+    name_for = {}
+    link_selections.
+      select { |x| x.link_class == 'name' }.
+      each do |link|
+      name_for[link.head_uuid] = link.name
+    end
+
+    uuids_to_add.each do |s|
+      name = name_for[s] || s
+      begin
+        Link.create(tail_uuid: @object.uuid,
+                    head_uuid: s,
+                    link_class: 'name',
+                    name: name)
+      rescue
+        Link.create(tail_uuid: @object.uuid,
+                    head_uuid: s,
+                    link_class: 'name',
+                    name: name + " (#{Time.now.localtime})")
+      end
     end
     redirect_to @object
   end
index a0cadb2b4c08e91356455b49a6cff4eac5a80360..6457cd0013456d6eac0831d54ffbe2744fddf56d 100644 (file)
@@ -1,5 +1,6 @@
 class ApplicationController < ActionController::Base
   include ArvadosApiClientHelper
+  include ApplicationHelper
 
   respond_to :html, :json, :js
   protect_from_forgery
@@ -63,23 +64,7 @@ class ApplicationController < ActionController::Base
     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
@@ -100,10 +85,44 @@ class ApplicationController < ActionController::Base
     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 show
     if !@object
       return render_not_found("object not found")
@@ -115,18 +134,38 @@ class ApplicationController < ActionController::Base
           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")
@@ -138,7 +177,7 @@ class ApplicationController < ActionController::Base
   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
@@ -177,6 +216,24 @@ class ApplicationController < ActionController::Base
     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|
@@ -225,7 +282,7 @@ class ApplicationController < ActionController::Base
   end
 
   def show_pane_list
-    %w(Attributes Metadata JSON API)
+    %w(Attributes Advanced)
   end
 
   protected
@@ -233,7 +290,7 @@ class ApplicationController < ActionController::Base
   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."
@@ -272,7 +329,13 @@ class ApplicationController < ActionController::Base
       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
@@ -307,7 +370,7 @@ class ApplicationController < ActionController::Base
             is_admin: u.is_admin,
             prefs: u.prefs
           }
-          if !request.format.json? and request.method == 'GET'
+          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
@@ -350,8 +413,18 @@ class ApplicationController < ActionController::Base
   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
 
@@ -385,7 +458,10 @@ class ApplicationController < ActionController::Base
   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|
@@ -472,14 +548,19 @@ class ApplicationController < ActionController::Base
     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
@@ -493,11 +574,68 @@ class ApplicationController < ActionController::Base
         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
index 6a5df8754ff9b69147e8f4669dba7f70df8a4d73..88dadbba626e6adf3329891f66feca27dfdaaedb 100644 (file)
@@ -3,11 +3,13 @@ class CollectionsController < ApplicationController
                      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
@@ -40,6 +42,20 @@ class CollectionsController < ApplicationController
     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]])
@@ -143,10 +159,10 @@ class CollectionsController < ApplicationController
       end
       @output_of = jobs_with.call(output: @object.uuid)
       @log_of = jobs_with.call(log: @object.uuid)
-      @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
-      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
diff --git a/apps/workbench/app/controllers/folders_controller.rb b/apps/workbench/app/controllers/folders_controller.rb
deleted file mode 100644 (file)
index 8ebb1a3..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-class FoldersController < ApplicationController
-  def model_class
-    Group
-  end
-
-  def index_pane_list
-    %w(Folders)
-  end
-
-  def remove_item
-    @removed_uuids = []
-    links = []
-    item = ArvadosBase.find params[:item_uuid]
-    if (item.class == Link and
-        item.link_class == 'name' and
-        item.tail_uuid = @object.uuid)
-      # Given uuid is a name link, linking an object to this
-      # folder. First follow the link to find the item we're removing,
-      # then delete the link.
-      links << item
-      item = ArvadosBase.find item.head_uuid
-    else
-      # Given uuid is an object. Delete all names.
-      links += Link.where(tail_uuid: @object.uuid,
-                          head_uuid: item.uuid,
-                          link_class: 'name')
-    end
-    links.each do |link|
-      @removed_uuids << link.uuid
-      link.destroy
-    end
-    if item.owner_uuid == @object.uuid
-      # Object is owned by this folder. Remove it from the folder by
-      # changing owner to the current user.
-      item.update_attributes owner_uuid: current_user
-      @removed_uuids << item.uuid
-    end
-  end
-
-  def index
-    @objects = Group.where(group_class: 'folder').order('name')
-    parent_of = {current_user.uuid => 'me'}
-    @objects.each do |ob|
-      parent_of[ob.uuid] = ob.owner_uuid
-    end
-    children_of = {false => [], 'me' => [current_user]}
-    @objects.each do |ob|
-      if ob.owner_uuid != current_user.uuid and
-          not parent_of.has_key? ob.owner_uuid
-        parent_of[ob.uuid] = false
-      end
-      children_of[parent_of[ob.uuid]] ||= []
-      children_of[parent_of[ob.uuid]] << ob
-    end
-    buildtree = lambda do |children_of, root_uuid=false|
-      tree = {}
-      children_of[root_uuid].andand.each do |ob|
-        tree[ob] = buildtree.call(children_of, ob.uuid)
-      end
-      tree
-    end
-    sorted_paths = lambda do |tree, depth=0|
-      paths = []
-      tree.keys.sort_by { |ob|
-        ob.is_a?(String) ? ob : ob.friendly_link_name
-      }.each do |ob|
-        paths << {object: ob, depth: depth}
-        paths += sorted_paths.call tree[ob], depth+1
-      end
-      paths
-    end
-    @my_folder_tree =
-      sorted_paths.call buildtree.call(children_of, 'me')
-    @shared_folder_tree =
-      sorted_paths.call({'Shared with me' =>
-                          buildtree.call(children_of, false)})
-  end
-
-  def choose
-    index
-    render partial: 'choose'
-  end
-
-  def show
-    @objects = @object.contents include_linked: true
-    @share_links = Link.filter([['head_uuid', '=', @object.uuid],
-                                ['link_class', '=', 'permission']])
-    @logs = Log.limit(10).filter([['object_uuid', '=', @object.uuid]])
-
-    @objects_and_names = []
-    @objects.each do |object|
-      if !(name_links = @objects.links_for(object, 'name')).empty?
-        name_links.each do |name_link|
-          @objects_and_names << [object, name_link]
-        end
-      else
-        @objects_and_names << [object,
-                               Link.new(tail_uuid: @object.uuid,
-                                        head_uuid: object.uuid,
-                                        link_class: "name",
-                                        name: "")]
-      end
-    end
-
-    super
-  end
-
-  def create
-    @new_resource_attrs = (params['folder'] || {}).merge(group_class: 'folder')
-    @new_resource_attrs[:name] ||= 'New folder'
-    super
-  end
-
-  def update
-    @updates = params['folder']
-    super
-  end
-end
index 71327d132e7bd6238accf48236bc45194f90d5ab..7698fdba934cb68e641ea23d7ffe92635aa09ffd 100644 (file)
@@ -1,6 +1,6 @@
 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
@@ -8,7 +8,10 @@ class GroupsController < ApplicationController
   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 b7526c949a2c6ea28daf6807fa8545350bf4ac6a..ff3ac6b98eba40ee3d1be2f34c72ec26fa9450ca 100644 (file)
@@ -53,6 +53,6 @@ class JobsController < ApplicationController
   end
 
   def show_pane_list
-    %w(Status Attributes Provenance Metadata JSON API)
+    %w(Status Details Provenance Advanced)
   end
 end
index 500927bdb62ba240301382cc0239a50f308d34ab..a4a9d69bef8205c7df6fd652546f52fd9ad0b6a7 100644 (file)
@@ -3,6 +3,42 @@ class PipelineInstancesController < ApplicationController
   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)
     return nil, nil if params['tab_pane'] != "Graph"
 
@@ -143,13 +179,17 @@ class PipelineInstancesController < ApplicationController
       :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
 
index 5173d4e376b9a2ae244ca90d05b3130f3e2e06d7..2be51c6a1804968e9785dae7d5381a84b913e5fe 100644 (file)
@@ -6,7 +6,6 @@ class PipelineTemplatesController < ApplicationController
   end
 
   def show_pane_list
-    %w(Components Pipelines Attributes Metadata JSON API)
+    %w(Components Pipelines Advanced)
   end
-
 end
diff --git a/apps/workbench/app/controllers/projects_controller.rb b/apps/workbench/app/controllers/projects_controller.rb
new file mode 100644 (file)
index 0000000..f73b6c0
--- /dev/null
@@ -0,0 +1,148 @@
+class ProjectsController < ApplicationController
+  def model_class
+    Group
+  end
+
+  def index_pane_list
+    %w(Projects)
+  end
+
+  def show_pane_list
+    %w(Contents Permissions Advanced)
+  end
+
+  def remove_item
+    params[:item_uuids] = [params[:item_uuid]]
+    remove_items
+    render template: 'projects/remove_items'
+  end
+
+  def remove_items
+    @removed_uuids = []
+    links = []
+    params[:item_uuids].collect { |uuid| ArvadosBase.find uuid }.each do |item|
+      if (item.class == Link and
+          item.link_class == 'name' and
+          item.tail_uuid == @object.uuid)
+        # Given uuid is a name link, linking an object to this
+        # project. First follow the link to find the item we're removing,
+        # then delete the link.
+        links << item
+        item = ArvadosBase.find item.head_uuid
+      else
+        # Given uuid is an object. Delete all names.
+        links += Link.where(tail_uuid: @object.uuid,
+                            head_uuid: item.uuid,
+                            link_class: 'name')
+      end
+      links.each do |link|
+        @removed_uuids << link.uuid
+        link.destroy
+      end
+      if item.owner_uuid == @object.uuid
+        # Object is owned by this project. Remove it from the project by
+        # changing owner to the current user.
+        item.update_attributes owner_uuid: current_user.uuid
+        @removed_uuids << item.uuid
+      end
+    end
+  end
+
+  def find_objects_for_index
+    @objects = Group.
+      filter([['group_class','in',['project','folder']]]).
+      order('name')
+    super
+    parent_of = {current_user.uuid => 'me'}
+    @objects.each do |ob|
+      parent_of[ob.uuid] = ob.owner_uuid
+    end
+    children_of = {false => [], 'me' => [current_user]}
+    @objects.each do |ob|
+      if ob.owner_uuid != current_user.uuid and
+          not parent_of.has_key? ob.owner_uuid
+        parent_of[ob.uuid] = false
+      end
+      children_of[parent_of[ob.uuid]] ||= []
+      children_of[parent_of[ob.uuid]] << ob
+    end
+    buildtree = lambda do |children_of, root_uuid=false|
+      tree = {}
+      children_of[root_uuid].andand.each do |ob|
+        tree[ob] = buildtree.call(children_of, ob.uuid)
+      end
+      tree
+    end
+    sorted_paths = lambda do |tree, depth=0|
+      paths = []
+      tree.keys.sort_by { |ob|
+        ob.is_a?(String) ? ob : ob.friendly_link_name
+      }.each do |ob|
+        paths << {object: ob, depth: depth}
+        paths += sorted_paths.call tree[ob], depth+1
+      end
+      paths
+    end
+    @my_project_tree =
+      sorted_paths.call buildtree.call(children_of, 'me')
+    @shared_project_tree =
+      sorted_paths.call({'Shared with me' =>
+                          buildtree.call(children_of, false)})
+  end
+
+  def show
+    @objects = @object.contents(limit: 50,
+                                include_linked: true,
+                                offset: params[:offset] || 0)
+    @share_links = Link.filter([['head_uuid', '=', @object.uuid],
+                                ['link_class', '=', 'permission']])
+    @logs = Log.limit(10).filter([['object_uuid', '=', @object.uuid]])
+
+    @objects_and_names = []
+    @objects.each do |object|
+      if !(name_links = @objects.links_for(object, 'name')).empty?
+        name_links.each do |name_link|
+          @objects_and_names << [object, name_link]
+        end
+      elsif object.respond_to? :name
+        @objects_and_names << [object, object]
+      else
+        @objects_and_names << [object,
+                               Link.new(owner_uuid: @object.uuid,
+                                        tail_uuid: @object.uuid,
+                                        head_uuid: object.uuid,
+                                        link_class: "name",
+                                        name: "")]
+      end
+    end
+    if params[:partial]
+      respond_to do |f|
+        f.json {
+          render json: {
+            content: render_to_string(partial: 'show_contents_rows.html',
+                                      formats: [:html],
+                                      locals: {
+                                        objects_and_names: @objects_and_names,
+                                        project: @object
+                                      }),
+            next_page_href: (next_page_offset and
+                             url_for(offset: next_page_offset, partial: true))
+          }
+        }
+      end
+    else
+      super
+    end
+  end
+
+  def create
+    @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
+    @new_resource_attrs[:name] ||= 'New project'
+    super
+  end
+
+  def update
+    @updates = params['project']
+    super
+  end
+end
index 3d8c8530add9aecd27898bc0325bfa44d8be165f..0313de5aa2e7b9559146b9cfca7a6f31b166862d 100644 (file)
@@ -1,6 +1,5 @@
 class UsersController < ApplicationController
   skip_before_filter :find_object_by_uuid, :only => [:welcome, :activity, :storage]
-  skip_around_filter :thread_with_mandatory_api_token, :only => :welcome
   before_filter :ensure_current_user_is_admin, only: [:sudo, :unsetup, :setup]
 
   def welcome
index 2b7ec147a47759c03985a3c4cccac2b725328a6e..66267e028d4df4eb5dd1436fcdeb507904e466aa 100644 (file)
@@ -79,10 +79,16 @@ module ApplicationHelper
   #
   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
@@ -124,7 +130,7 @@ module ApplicationHelper
       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.
@@ -136,8 +142,10 @@ module ApplicationHelper
     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'
@@ -165,16 +173,26 @@ module ApplicationHelper
       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={})
@@ -210,7 +228,7 @@ module ApplicationHelper
     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
 
@@ -223,6 +241,52 @@ module ApplicationHelper
       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'
@@ -236,15 +300,6 @@ module ApplicationHelper
       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 = []
@@ -297,8 +352,9 @@ module ApplicationHelper
       "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)
@@ -327,4 +383,48 @@ module ApplicationHelper
               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
diff --git a/apps/workbench/app/helpers/folders_helper.rb b/apps/workbench/app/helpers/folders_helper.rb
deleted file mode 100644 (file)
index d27e7b4..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-module FoldersHelper
-end
index 7b6fb727637fc6dc0e56fac95c303321bf050abc..35d28d542b05ad8a9c71a94fc43fd1f5928d47f9 100644 (file)
@@ -82,17 +82,22 @@ module PipelineInstancesHelper
       end
       if pj[:job][:success]
         pj[:result] = 'complete'
+        pj[:labeltype] = 'success'
         pj[:complete] = true
         pj[:progress] = 1.0
       elsif pj[:job][:finished_at]
         pj[:result] = 'failed'
+        pj[:labeltype] = 'danger'
         pj[:failed] = true
       elsif pj[:job][:started_at]
         pj[:result] = 'running'
+        pj[:labeltype] = 'primary'
       elsif pj[:job][:uuid]
         pj[:result] = 'queued'
+        pj[:labeltype] = 'default'
       else
         pj[:result] = 'none'
+        pj[:labeltype] = 'default'
       end
       pj[:job_id] = pj[:job][:uuid]
       pj[:script] = pj[:job][:script] || c[:script]
diff --git a/apps/workbench/app/helpers/projects_helper.rb b/apps/workbench/app/helpers/projects_helper.rb
new file mode 100644 (file)
index 0000000..db5c5ce
--- /dev/null
@@ -0,0 +1,2 @@
+module ProjectsHelper
+end
index 33e107e3693c94b4954f4e155312b060ab50205e..aca87868c3ab7f1f3523fc66d6569f031c8e99f2 100644 (file)
@@ -111,6 +111,10 @@ class ArvadosBase < ActiveRecord::Base
     new.private_reload(hash)
   end
 
+  def self.find?(*args)
+    find(*args) rescue nil
+  end
+
   def self.order(*args)
     ArvadosResourceList.new(self).order(*args)
   end
@@ -274,8 +278,9 @@ class ArvadosBase < ActiveRecord::Base
     uuid
   end
 
-  def dup
-    super.forget_uuid!
+  def initialize_copy orig
+    super
+    forget_uuid!
   end
 
   def attributes_for_display
@@ -287,14 +292,14 @@ class ArvadosBase < ActiveRecord::Base
   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
 
@@ -349,8 +354,12 @@ class ArvadosBase < ActiveRecord::Base
     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
@@ -361,6 +370,27 @@ class ArvadosBase < ActiveRecord::Base
     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 a64f7e1e10c6c3ee635689cae09403963868198d..008d108fd43ed8a7e0a3abe45cc380492b03ba4b 100644 (file)
@@ -1,6 +1,4 @@
 class Collection < ArvadosBase
-  include ApplicationHelper
-
   MD5_EMPTY = 'd41d8cd98f00b204e9800998ecf8427e'
 
   # Return true if the given string is the locator of a zero-length blob
@@ -8,12 +6,12 @@ class Collection < ArvadosBase
     !!locator.to_s.match("^#{MD5_EMPTY}(\\+.*)?\$")
   end
 
-  def self.goes_in_folders?
+  def self.goes_in_projects?
     true
   end
 
   def content_summary
-    human_readable_bytes_html(total_bytes) + " " + super
+    ApplicationController.helpers.human_readable_bytes_html(total_bytes) + " " + super
   end
 
   def total_bytes
@@ -23,10 +21,13 @@ class Collection < ArvadosBase
         tot += file[2]
       end
       tot
+    else
+      0
     end
   end
 
   def files_tree
+    return [] if files.empty?
     tree = files.group_by { |file_spec| File.split(file_spec.first) }
     # Fill in entries for empty directories.
     tree.keys.map { |basedir, _| File.split(basedir) }.each do |splitdir|
index 638f6e884abf5fca1df9768bbf5eca56184de8d3..9e627bf66ea012871ee8a536799d79ae40de3e9a 100644 (file)
@@ -1,5 +1,5 @@
 class Group < ArvadosBase
-  def self.goes_in_folders?
+  def self.goes_in_projects?
     true
   end
 
@@ -13,7 +13,7 @@ class Group < ArvadosBase
   end
 
   def class_for_display
-    group_class == 'folder' ? 'Folder' : super
+    group_class.in?(['folder', 'project']) ? 'Project' : super
   end
 
   def editable?
index 3880f0513d6f2edbe7fd3a38300e430a29f5ca58..7c2d3e439686c848980d46f920b72f382d71fa4c 100644 (file)
@@ -1,5 +1,5 @@
 class Human < ArvadosBase
-  def self.goes_in_folders?
+  def self.goes_in_projects?
     true
   end
 end
index 173d3a06964fb5667b9546aee4bab518baf3c190..aac6168d22aecac8d37d9afcaee56db844cecdcd 100644 (file)
@@ -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
@@ -11,6 +15,21 @@ class Job < ArvadosBase
     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
index 5e7b42a60b0ea985dcda2daf6822d8dcb22c1dec..868082b3ed9980652c24a9bc9eae451c620f0dd5 100644 (file)
@@ -4,4 +4,8 @@ class Link < ArvadosBase
   def self.by_tail(t, opts={})
     where(opts.merge :tail_uuid => t.uuid)
   end
+
+  def default_name
+    self.class.resource_class_for_uuid(head_uuid).default_name rescue super
+  end
 end
index 5a88003f8281199547431ca8051eea980c0a9d0f..c3b14755f3fb6f8dbdf77d3630e689797cca8eea 100644 (file)
@@ -1,10 +1,18 @@
 class PipelineInstance < ArvadosBase
   attr_accessor :pipeline_template
 
-  def self.goes_in_folders?
+  def self.goes_in_projects?
     true
   end
 
+  def content_summary
+    begin
+      PipelineTemplate.find(pipeline_template_uuid).name
+    rescue
+      super
+    end
+  end
+
   def update_job_parameters(new_params)
     self.components[:steps].each_with_index do |step, i|
       step[:params].each do |param|
index 82e3d307c01827b24857762cf2d914cf58a8523a..e1af2cb052676525ddba49d3f30afc07ff2aff10 100644 (file)
@@ -1,5 +1,5 @@
 class PipelineTemplate < ArvadosBase
-  def self.goes_in_folders?
+  def self.goes_in_projects?
     true
   end
 
index 68c0038b6363e235cefb4db8306d27fbe3597034..7c611e47ff242ea99de70a12c8ea43d1d1f9046b 100644 (file)
@@ -1,5 +1,5 @@
 class Specimen < ArvadosBase
-  def self.goes_in_folders?
+  def self.goes_in_projects?
     true
   end
 end
index cf7099d2fe4bf67e3250552a81d94f42c4a441f4..e7a6ceb70a90357a12cc7f9e0c27240662cf020f 100644 (file)
@@ -1,5 +1,5 @@
 class Trait < ArvadosBase
-  def self.goes_in_folders?
+  def self.goes_in_projects?
     true
   end
 end
index 3efca7299549d38f095bebf8595980fd0dbe800c..8f82b0469638b535ff86a7ff55b87d92277a05d9 100644 (file)
@@ -3,7 +3,7 @@
     <%= message %><br />
   <% end %>
 <% else %>
-      <% if obj.attribute_editable?(attr) %>
+      <% if obj.attribute_editable?(attr) and (!defined?(editable) || editable) %>
         <%= render_editable_attribute obj, attr %>
         <% if resource_class_for_uuid(attrvalue, {referring_object: obj, referring_attr: attr}) %>
        <br />
diff --git a/apps/workbench/app/views/application/_choose.html.erb b/apps/workbench/app/views/application/_choose.html.erb
new file mode 100644 (file)
index 0000000..c42adac
--- /dev/null
@@ -0,0 +1,31 @@
+<div class="modal arv-choose">
+  <div class="modal-dialog" style="width:80%">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" onClick="reset_form()" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <h4 class="modal-title"><%= params[:title] || "Choose #{@objects.first.class_for_display}" %></h4>
+      </div>
+      <div class="modal-body">
+        <div>
+          <div class="row">
+            <div class="col-sm-6">
+            </div>
+            <div class="col-sm-6">
+              <input type="text" class="form-control filterable-control" placeholder="Search" data-filterable-target=".modal.arv-choose .selectable-container"/>
+            </div>
+          </div>
+        </div>
+        <div class="container-fluid arv-filterable-list selectable-container" style="height: 20em; overflow-y: scroll" data-infinite-scroller="#choose-scroll" id="choose-scroll" data-infinite-content-href="<%= @next_page_href %>">
+          <%= render partial: 'choose_rows', locals: {multiple: multiple} %>
+        </div>
+      </div>
+
+      <div class="modal-footer">
+        <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Cancel</button>
+        <button class="btn btn-primary" aria-hidden="true" data-enable-if-selection disabled><%= raw(params[:action_name]) || 'Select' %></button>
+        <div class="modal-error hide" style="text-align: left; margin-top: 1em;">
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
similarity index 88%
rename from apps/workbench/app/views/folders/_choose.js.erb
rename to apps/workbench/app/views/application/_choose.js.erb
index 4ecd0721216cdaa9e45182fa09429b5ae3efff5c..3bb369451add5674495a5747540aa72ef7f53f12 100644 (file)
@@ -1,4 +1,4 @@
-$('body > .modal-container').html("<%= escape_javascript(render partial: 'choose.html') %>");
+$('body > .modal-container').html("<%= escape_javascript(render partial: 'choose.html', locals: {multiple: multiple}) %>");
 $('body > .modal-container .modal').modal('show');
 $('body > .modal-container .modal .modal-footer .btn-primary').
     addClass('<%= j params[:action_class] %>').
index 353bd74143103f7d87f0636d0668a2de4e60b280..b7f27df3d79539bbe7c01b86dadbf0303160dc78 100644 (file)
@@ -1,3 +1,24 @@
+<% 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 = {};
 
@@ -60,7 +81,7 @@
   <% 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;">
       <div class="pane-content">
         <% if i == 0 %>
           <%= render(partial: 'show_' + pane.downcase,
index c7ff33b16e57bcd474134573ceb266a3e5ecf720..426a497a605111eb8e56e1c60e114b2dda393f17 100644 (file)
@@ -1,3 +1,6 @@
 <%= content_for :content_top %>
-<%= content_for :tab_line_buttons %>
+<br clear="all" />
+<div class="pull-right">
+  <%= content_for :tab_line_buttons %>
+</div>
 <%= content_for :tab_panes %>
index 69ed9dca74faabd39e0294e5c81b12ebb77b4dcc..6d6383e24ec02c6f460f69e738ace017d52082d1 100644 (file)
@@ -1,5 +1,5 @@
 <% if object.editable? %>
-  <%= link_to({action: 'destroy', id: object.uuid}, method: :delete, remote: true, data: {confirm: "You are about to delete #{object.class_for_display.downcase} '#{object.friendly_link_name}' (#{object.uuid}).\n\nAre you sure?"}) do %>
+  <%= link_to({action: 'destroy', id: object.uuid}, method: :delete, remote: true, data: {confirm: "Really delete #{object.class_for_display.downcase} '#{object.friendly_link_name}'?"}) do %>
     <i class="glyphicon glyphicon-trash"></i>
   <% end %>
 <% end %>
index 9645455ace5975b59eb69dbd29735ba5469caa24..9d279bc402fac12dfd433075c5f2876dbaab4394 100644 (file)
@@ -1,4 +1,4 @@
-<%if object and object.class.goes_in_folders? %>
+<%if object and object.class.goes_in_projects? %>
   <% fn = if defined? friendly_name
             friendly_name
           else
diff --git a/apps/workbench/app/views/application/_show_advanced.html.erb b/apps/workbench/app/views/application/_show_advanced.html.erb
new file mode 100644 (file)
index 0000000..e8474f6
--- /dev/null
@@ -0,0 +1,23 @@
+<div class="panel-group" id="arv-adv-accordion">
+  <% ['Metadata',
+     'API response',
+     'Python example',
+     'CLI example',
+     'curl example'].each do |section| %>
+    <% section_id = section.gsub(" ","_").downcase %>
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h4 class="panel-title">
+          <a data-toggle="collapse" data-parent="#arv-adv-accordion" href="#advanced_<%=section_id%>">
+            <%= section %>
+          </a>
+        </h4>
+      </div>
+      <div id="advanced_<%=section_id%>" class="panel-collapse collapse">
+        <div class="panel-body">
+          <%= render partial: "show_advanced_#{section_id}", locals: {object: @object} %>
+        </div>
+      </div>
+    </div>
+  <% end %>
+</div>
diff --git a/apps/workbench/app/views/application/_show_advanced_api_response.html.erb b/apps/workbench/app/views/application/_show_advanced_api_response.html.erb
new file mode 100644 (file)
index 0000000..9ef124a
--- /dev/null
@@ -0,0 +1,3 @@
+<pre>
+<%= JSON.pretty_generate(object.attributes.reject { |k,v| k == 'id' }) rescue nil %>
+</pre>
diff --git a/apps/workbench/app/views/application/_show_advanced_cli_example.html.erb b/apps/workbench/app/views/application/_show_advanced_cli_example.html.erb
new file mode 100644 (file)
index 0000000..4516876
--- /dev/null
@@ -0,0 +1,8 @@
+<pre>
+arv --pretty <%= object.class.to_s.underscore %> get \
+ --uuid <%= object.uuid %>
+
+arv <%= object.class.to_s.underscore %> update \
+ --uuid <%= object.uuid %> \
+ --<%= object.class.to_s.underscore.gsub '_', '-' %> '<%= JSON.generate({object.attributes.keys[-3] => object.attributes.values[-3]}).gsub("'","'\''") %>'
+</pre>
diff --git a/apps/workbench/app/views/application/_show_advanced_curl_example.html.erb b/apps/workbench/app/views/application/_show_advanced_curl_example.html.erb
new file mode 100644 (file)
index 0000000..0454c13
--- /dev/null
@@ -0,0 +1,9 @@
+<pre>
+curl -X PUT \
+ -H "Authorization: OAuth2 $ARVADOS_API_TOKEN" \
+ --data-urlencode <%= object.class.to_s.underscore %>@/dev/stdin \
+ https://$ARVADOS_API_HOST/arvados/v1/<%= object.class.to_s.pluralize.underscore %>/<%= object.uuid %> \
+ &lt;&lt;EOF
+<%= JSON.pretty_generate({object.attributes.keys[-3] => object.attributes.values[-3]}) %>
+EOF
+</pre>
diff --git a/apps/workbench/app/views/application/_show_advanced_metadata.html.erb b/apps/workbench/app/views/application/_show_advanced_metadata.html.erb
new file mode 100644 (file)
index 0000000..c036b36
--- /dev/null
@@ -0,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 %>
diff --git a/apps/workbench/app/views/application/_show_advanced_python_example.html.erb b/apps/workbench/app/views/application/_show_advanced_python_example.html.erb
new file mode 100644 (file)
index 0000000..3ceae4f
--- /dev/null
@@ -0,0 +1,5 @@
+<pre>
+import arvados
+
+x = arvados.api().<%= object.class.to_s.pluralize.underscore %>().get(uuid='<%= object.uuid %>').execute()
+</pre>
diff --git a/apps/workbench/app/views/application/_show_json.html.erb b/apps/workbench/app/views/application/_show_json.html.erb
deleted file mode 100644 (file)
index 2f0cd21..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<pre>
-<%= JSON.pretty_generate(@object.attributes.reject { |k,v| k == 'id' }) rescue nil %>
-</pre>
diff --git a/apps/workbench/app/views/application/_show_metadata.html.erb b/apps/workbench/app/views/application/_show_metadata.html.erb
deleted file mode 100644 (file)
index 77f2dda..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-<% 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
-%>
-
-<h3>Metadata about this object</h3>
-<% if outgoing.items_available > 0 %>
-<table class="table topalign">
-  <thead>
-    <tr>
-      <th>metadata uuid</th>
-      <th>class</th>
-      <th>name</th>
-      <th>properties</th>
-      <th>object</th>
-    </tr>
-  </thead>
-  <tbody>
-    <% outgoing.each do |link| %>
-      <tr>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "uuid", attrvalue: link.uuid } %></td>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "link_class", attrvalue: link.link_class } %></td>
-        <td><%= render_editable_attribute link, 'name' %></td>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "properties", attrvalue: link.properties } %></td>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "head_uuid", attrvalue: link.head_uuid } %></td>
-      </tr>
-    <% end %>
-  </tbody>
-</table>
-<% else %>
-No metadata.
-<% end %>
-
-<h3>Metadata that refers to this object</h3>
-<% if incoming.items_available > 0 %>
-<table class="table topalign">
-  <thead>
-    <tr>
-      <th>metadata uuid</th>
-      <th>subject</th>
-      <th>class</th>
-      <th>name</th>
-      <th>properties</th>
-    </tr>
-  </thead>
-  <tbody>
-    <% incoming.each do |link| %>
-      <tr>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "uuid", attrvalue: link.uuid } %></td>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "tail_uuid", attrvalue: link.tail_uuid } %></td>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "link_class", attrvalue: link.link_class } %></td>
-        <td><%= render_editable_attribute link, 'name' %></td>
-        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "properties", attrvalue: link.properties } %></td>
-      </tr>
-    <% end %>
-  </tbody>
-</table>
-<% else %>
-No metadata.
-<% end %>
index 81ca7453b204894a8eb5b8bbeade785afe1bbe04..03a5325b6d706ad173cb3486639ef54e6aee3672 100644 (file)
@@ -1,3 +1,15 @@
 <% htmloptions = {class: ''}.merge(htmloptions || {})
-   htmloptions[:class] += " btn-#{size}" rescue nil %>
-<%= link_to_if_arvados_object object, { link_text: raw('Show <i class="fa fa-fw fa-arrow-circle-right"></i>') }, { class: 'btn btn-default ' + htmloptions[:class] } %>
+   htmloptions[:class] += " btn-#{size}" rescue nil
+   link_text = 'Show' unless defined?(link_text) and link_text
+ %>
+<%= link_to_if_arvados_object object, {
+      link_text: raw('<i class="fa fa-fw ' + fa_icon_class_for_object(object) + '"></i> ' + link_text),
+      name_link: (defined?(name_link) && name_link && name_link.uuid) ? name_link : nil
+    }, {
+      data: {
+        toggle: 'tooltip',
+        placement: 'top'
+      },
+      title: 'show ' + object.class_for_display.downcase,
+      class: 'btn btn-default ' + htmloptions[:class],
+    } %>
diff --git a/apps/workbench/app/views/application/_show_object_description_cell.html.erb b/apps/workbench/app/views/application/_show_object_description_cell.html.erb
new file mode 100644 (file)
index 0000000..7ad3f0e
--- /dev/null
@@ -0,0 +1,2 @@
+<%= object.content_summary %>
+
index 800263f0f42bbabaae4897fbe732c13f64d85477..d94dd84be4d3a13b7da3b6facfb4264b39b7284a 100644 (file)
@@ -15,7 +15,7 @@
 <table class="table table-condensed arv-index">
   <thead>
     <tr>
-      <% if objects.first and objects.first.class.goes_in_folders? %>
+      <% if objects.first and objects.first.class.goes_in_projects? %>
         <th></th>
       <% end %>
       <th></th>
@@ -34,7 +34,7 @@
   <tbody>
     <% objects.each do |object| %>
     <tr data-object-uuid="<%= object.uuid %>">
-      <% if objects.first.class.goes_in_folders? %>
+      <% if objects.first.class.goes_in_projects? %>
         <td>
           <%= render :partial => "selection_checkbox", :locals => {:object => object} %>
         </td>
index 20269621f3f6b9b166b7b11aa79001682302c570..b35d7068bc7bfc814039bae80f7f598d136234b1 100644 (file)
@@ -7,8 +7,8 @@
  border-width: 1px;
  border-color: gray;
  position: absolute;
- left: 225px;
- right: 1px;
+ left: 25px;
+ right: 25px;
 }
 path:hover {
 stroke-width: 5;
index 20af6485b088c67f4ad6ae612ffc63916ead8c4e..0e72f7a2ddadf4f7947cb08b2afc0aa90a5cde50 100644 (file)
@@ -7,14 +7,18 @@
   <% if controller.model_class.creatable? %>
 
     <% if controller.model_class.name == 'User' %>
-      <%= link_to "Add a new #{controller.model_class.to_s.underscore.gsub '_', ' '}", setup_user_popup_path,
-        {class: 'btn btn-primary pull-right', :remote => true, 'data-toggle' =>  "modal",
-          'data-target' => '#user-setup-modal-window', return_to: request.url}  %>
+      <%= link_to setup_user_popup_path,
+        {class: 'btn btn-sm btn-primary', :remote => true, 'data-toggle' =>  "modal",
+          'data-target' => '#user-setup-modal-window', return_to: request.url} do %>
+        <i class="fa fa-fw fa-plus"></i> Add a new user
+      <% end %>
       <div id="user-setup-modal-window" class="modal fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"></div>
     <% else %>
-      <%= button_to "Add a new #{controller.controller_name.singularize.humanize.downcase}",
-        { action: 'create' },
-        { class: 'btn btn-primary pull-right' } %>
+      <%= button_to({action: 'create'}, {class: 'btn btn-sm btn-primary'}) do %>
+        <i class="fa fa-fw fa-plus"></i>
+        Add a new
+        <%= controller.controller_name.singularize.humanize.downcase %>
+      <% end %>
     <% end %>
 
   <% end %>
index 9da8ea1518d3ecf9e5bcf47446220659a6f90227..d6eca3a2155d2e7c50b58e030884fc5c8f323dcc 100644 (file)
@@ -1,6 +1,7 @@
 <% content_for :page_title do %>
   <%= (@object.respond_to?(:properties) ? @object.properties[:page_title] : nil) ||
-        @object.friendly_link_name %>
+      @name_link.andand.name ||
+      @object.friendly_link_name %>
 <% end %>
 
 <% content_for :content_top do %>
diff --git a/apps/workbench/app/views/collections/_choose.js.erb b/apps/workbench/app/views/collections/_choose.js.erb
new file mode 120000 (symlink)
index 0000000..8420a7f
--- /dev/null
@@ -0,0 +1 @@
+../application/_choose.js.erb
\ No newline at end of file
diff --git a/apps/workbench/app/views/collections/_choose_rows.html.erb b/apps/workbench/app/views/collections/_choose_rows.html.erb
new file mode 100644 (file)
index 0000000..650a8c8
--- /dev/null
@@ -0,0 +1,17 @@
+<% @name_links.each do |name_link| %>
+  <% if (object = get_object(name_link.head_uuid)) %>
+    <div class="row filterable selectable <%= 'multiple' if multiple %>" data-object-uuid="<%= name_link.uuid %>">
+      <div class="col-sm-12" style="overflow-x:hidden">
+        <i class="fa fa-fw fa-archive"></i>
+        <%= name_link.name %>
+      </div>
+      <div class="col-sm-11 col-sm-push-1" style="overflow-x: hidden">
+        <%= render_controller_partial(
+            'show_object_description_cell.html',
+            controller_name: 'collections',
+            locals: {object: object})
+            %>
+      </div>
+    </div>
+  <% end %>
+<% end %>
index 207ae13a740885f7099b9701ff1dfacb082f9725..deba78e47b7f2723b2fa10da36a7674b357a123f 100644 (file)
@@ -7,9 +7,11 @@
       <% friendly_name += raw(" <span class='label label-info'>#{tag_link.name}</span>") %>
     <% end %>
     <%= render :partial => "selection_checkbox", :locals => {:object => c, :friendly_name => friendly_name} %>
+
+    <%= render :partial => "show_object_button", :locals => {object: c, size: 'xs'} %>
   </td>
   <td>
-    <%= link_to_if_arvados_object c.uuid, {:no_tags => true } %>
+    <%= c.uuid %>
   </td>
   <td>
     <% i = 0 %>
index 74e02f79fe0c87a18a56b99dfa6c5ab3e0c645ff..c461163651c570ac635c69a16e91e3eabcf07b23 100644 (file)
@@ -1,14 +1,6 @@
 <% content_for :tab_line_buttons do %>
-<div class="row">
-  <div class="col-md-6"></div>
-  <div class="col-md-6">
-    <div class="pull-right">
-      <span style="padding-left: 1em">Collection storage status:</span>
-      <%= render partial: 'toggle_persist', locals: { uuid: @object.uuid, current_state: (@is_persistent ? 'persistent' : 'cache') } %>
-
-    </div>
-  </div>
-</div>
+<span style="padding-left: 1em">Collection storage status:</span>
+<%= render partial: 'toggle_persist', locals: { uuid: @object.uuid, current_state: (@is_persistent ? 'persistent' : 'cache') } %>
 <% end %>
 
 <% file_tree = @object.andand.files_tree %>
diff --git a/apps/workbench/app/views/collections/_show_object_description_cell.html.erb b/apps/workbench/app/views/collections/_show_object_description_cell.html.erb
new file mode 100644 (file)
index 0000000..1f1325d
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="nowrap">
+  <span class="deemphasize">
+    Files (<%= human_readable_bytes_html(object.total_bytes) %>):
+  </span><span class="arvados-filename">
+    <% object.files.each do |path, file, size| %>
+      <%= file %>
+    <% end %>
+  </span>
+  <div>
+    <% Link.filter([['link_class','=','tag'],['head_uuid','=',object.uuid]]).collect(&:name).each do |tagname| %>
+      <span class="label label-info"><%= tagname %></span>
+    <% end %>
+  </div>
+</div>
index 2f4ab64771158001cc6fad08833cfd6456cead5b..80640ad3991bb20ada08bc5154a4ac2806f5f685 100644 (file)
 
 <table id="collections-index" class="topalign table table-condensed table-fixedlayout"> <!-- table-fixed-header-row -->
   <colgroup>
-    <col width="4%" />
+    <col width="8%" />
     <col width="10%" />
     <col width="34%" />
-    <col width="15%" />
+    <col width="11%" />
     <col width="12%" />
     <col width="29%" />
   </colgroup>
index 710388d643d17099e99d2697caace53f57ead368..f91357b12067b4f4662bdfaebd6cf3ccfac81490 100644 (file)
         <% 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 %>
diff --git a/apps/workbench/app/views/folders/_show_folders.html.erb b/apps/workbench/app/views/folders/_show_folders.html.erb
deleted file mode 100644 (file)
index 2ecfd6e..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<div class="container-fluid arv-folder-list">
-  <% [@my_folder_tree, @shared_folder_tree].each do |tree| %>
-    <% tree.each do |foldernode| %>
-      <% rowtype = foldernode[:object].class %>
-      <div class="<%= 'folder' if rowtype == Group %> row" style="padding-left: <%= 1 + foldernode[:depth] %>em;">
-        <% if rowtype == String %>
-          <i class="fa fa-fw fa-folder-open-o"></i>
-          <%= foldernode[:object] %>
-        <% elsif rowtype == User %>
-          <% if foldernode[:object].uuid == current_user.andand.uuid %>
-            <i class="fa fa-fw fa-folder-open-o"></i>
-            My Folders
-          <% else %>
-            <i class="fa fa-fw fa-folder-o"></i>
-            <%= foldernode[:object].friendly_link_name %>
-          <% end %>
-        <% else %>
-          <i class="fa fa-fw fa-folder-o"></i>
-          <%= link_to foldernode[:object] do %>
-            <%= foldernode[:object].friendly_link_name %>
-          <% end %>
-          <div class="pull-right">
-            <%= render partial: 'delete_object_button', locals: {object: foldernode[:object]} %>
-          </div>
-        <% end %>
-      </div>
-    <% end %>
-  <% end %>
-</div>
diff --git a/apps/workbench/app/views/folders/show.html.erb b/apps/workbench/app/views/folders/show.html.erb
deleted file mode 100644 (file)
index ebf504e..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-<div class="row row-fill-height">
-  <div class="col-md-6">
-    <div class="panel panel-info">
-      <div class="panel-heading">
-        <h3 class="panel-title">
-          <%= render_editable_attribute @object, 'name', nil, {data: {emptytext: "New folder"}} %>
-        </h3>
-      </div>
-      <div class="panel-body">
-        <img src="/favicon.ico" class="pull-right" alt="" style="opacity: 0.3"/>
-        <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "Created: #{@object.created_at.to_s(:long)}", 'data-toggle' => 'manual', 'id' => "#{@object.uuid}-description" } %>
-        <% if @object.attribute_editable? 'description' %>
-        <div style="margin-top: 1em;">
-          <a href="#" class="btn btn-xs btn-default" data-toggle="x-editable" data-toggle-selector="#<%= @object.uuid %>-description"><i class="fa fa-fw fa-pencil"></i> Edit description</a>
-        </div>
-        <% end %>
-      </div>
-    </div>
-  </div>
-  <div class="col-md-3">
-    <div class="panel panel-default">
-      <div class="panel-heading">
-        <h3 class="panel-title">
-          Activity
-        </h3>
-      </div>
-      <div class="panel-body smaller-text">
-        <!--
-        <input type="text" class="form-control" placeholder="Search"/>
-        -->
-        <div style="height:0.5em;"></div>
-        <% if @logs.any? %>
-          <%= render_arvados_object_list_start(@logs, 'Show all activity',
-                logs_path(filters: [['object_uuid','=',@object.uuid]].to_json)) do |log| %>
-          <p>
-          <%= time_ago_in_words(log.event_at) %> ago: <%= log.summary %>
-            <% if log.object_uuid %>
-            <%= link_to_if_arvados_object log.object_uuid, link_text: raw('<i class="fa fa-hand-o-right"></i>') %>
-            <% end %>
-          </p>
-          <% end %>
-        <% else %>
-          <p>
-            Created: <%= @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 %>
-          </p>
-        <% end %>
-      </div>
-    </div>
-  </div>
-  <div class="col-md-3">
-    <div class="panel panel-default">
-      <div class="panel-heading">
-        <h3 class="panel-title">
-          Sharing and permissions
-        </h3>
-      </div>
-      <div class="panel-body">
-        <!--
-        <input type="text" class="form-control" placeholder="Search"/>
-        -->
-        <div style="height:0.5em;"></div>
-        <% if @object.owner %>
-          <p>Permissions inherited from:
-            <br />
-            <% if User == resource_class_for_uuid(@object.owner_uuid) %>
-              <i class="fa fa-fw fa-user"></i>
-            <% else %>
-              <i class="fa fa-fw fa-folder"></i>
-            <% end %>
-            <%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %>
-            <%= button_to('Move to...',
-                choose_folders_path(
-                 title: 'Move to...',
-                 editable: true,
-                 action_name: 'Move',
-                 action_href: folder_path(@object.uuid),
-                 action_method: 'put',
-                 action_data: {selection_param: 'folder[owner_uuid]'}.to_json),
-                { class: "btn btn-default btn-xs arv-move-to-folder", remote: true, method: 'get' }) %>
-          </p>
-          <hr />
-        <% end %>
-        <p>
-          <% if not @share_links.any? %>
-            <span class="deemphasize">(No additional permissions)</span>
-          <% else %>
-            Also shared with:
-            <% @share_links.andand.each do |link| %>
-              <br /><%= link_to_if_arvados_object link.tail_uuid, friendly_name: true %>
-            <% end %>
-          <% end %>
-        </p>
-      </div>
-    </div>
-  </div>
-</div>
-
-<% if @show_cards %>
-<!-- disable cards section until we have bookmarks -->
-<div class="row">
-  <% @objects[0..3].each do |object| %>
-  <div class="card arvados-object">
-    <div class="card-top blue">
-      <a href="#">
-        <img src="/favicon.ico" alt=""/>
-      </a>
-    </div>
-    <div class="card-info">
-      <span class="title"><%= @objects.name_for(object) || object.class_for_display %></span>
-      <div class="desc"><%= object.respond_to?(:description) ? object.description : object.uuid %></div>
-    </div>
-    <div class="card-bottom">
-      <%= render :partial => "show_object_button", :locals => {object: object, htmloptions: {class: 'btn-default btn-block'}} %>
-    </div>
-  </div>
-  <% end %>
-</div>
-<!-- end disabled cards section -->
-<% end %>
-
-<div class="row">
-  <div class="col-md-12">
-    <div class="panel panel-info">
-      <div class="panel-heading">
-        <div class="row">
-          <div class="col-md-6">
-            <h3 class="panel-title" style="vertical-align:middle;">
-              Contents
-            </h3>
-          </div>
-          <div class="col-md-6">
-            <div class="input-group input-group-sm pull-right">
-              <input type="text" class="form-control search-folder-contents" placeholder="Search folder contents"/>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="panel-body">
-        <p>
-        </p>
-        <table class="table table-condensed arv-index">
-          <tbody>
-            <colgroup>
-              <col width="3%" />
-              <col width="8%" />
-              <col width="30%" />
-              <col width="15%" />
-              <col width="15%" />
-              <col width="20%" />
-              <col width="8%" />
-            </colgroup>
-            <% @objects_and_names.each do |object, name_link| %>
-              <tr data-object-uuid="<%= (name_link && name_link.uuid) || object.uuid %>">
-                <td>
-                  <%= render :partial => "selection_checkbox", :locals => {object: object} %>
-                </td>
-                <td>
-                  <%= render :partial => "show_object_button", :locals => {object: object, size: 'xs'} %>
-                </td>
-                <td>
-                  <%= render_editable_attribute name_link, 'name', nil, {data: {emptytext: "Unnamed #{object.class_for_display}"}} %>
-                </td>
-                <td>
-                  <%= object.content_summary %>
-                </td>
-                <td title="<%= object.modified_at %>">
-                  <span>
-                    <%= raw distance_of_time_in_words(object.modified_at, Time.now).sub('about ','~').sub(' ','&nbsp;') + '&nbsp;ago' rescue object.modified_at %>
-                  </span>
-                </td>
-                <td class="arvados-uuid">
-                  <%= object.uuid %>
-                </td>
-                <td>
-                  <% if @object.editable? %>
-                    <%= link_to({action: 'remove_item', id: @object.uuid, item_uuid: ((name_link && name_link.uuid) || object.uuid)}, method: :delete, remote: true, data: {confirm: "You are about to remove #{object.class_for_display} #{object.uuid} from this folder.\n\nAre you sure?"}, class: 'btn btn-xs btn-default') do %>
-                      Remove <i class="fa fa-fw fa-ban"></i>
-                    <% end %>
-                  <% end %>
-                </td>
-              </tr>
-            <% end %>
-          </tbody>
-          <thead>
-            <tr>
-              <th>
-              </th>
-              <th>
-              </th>
-              <th>
-                name
-              </th>
-              <th>
-                type
-              </th>
-              <th>
-                modified
-              </th>
-              <th>
-                uuid
-              </th>
-              <th>
-              </th>
-            </tr>
-          </thead>
-        </table>
-        <p></p>
-      </div>
-    </div>
-  </div>
-</div>
diff --git a/apps/workbench/app/views/jobs/_show_details.html.erb b/apps/workbench/app/views/jobs/_show_details.html.erb
new file mode 100644 (file)
index 0000000..7b6b176
--- /dev/null
@@ -0,0 +1 @@
+<%= render partial: 'application/show_attributes' %>
diff --git a/apps/workbench/app/views/jobs/_show_object_description_cell.html.erb b/apps/workbench/app/views/jobs/_show_object_description_cell.html.erb
new file mode 100644 (file)
index 0000000..6788fc1
--- /dev/null
@@ -0,0 +1,15 @@
+<div class="nowrap">
+  <div class="row">
+    <div class="col-sm-2 inline-progress-container">
+      <%= render partial: 'job_progress', locals: {j: object} %>
+    </div>
+    <div class="col-sm-10">
+      <%= object.script %>
+      <span class="deemphasize">
+        job
+        using <%= object.script_version %> commit
+        from <%= object.repository %> repository
+      </span>
+    </div>
+  </div>
+</div>
index 2948ec23519706b1eb3cd78996fabe02b47915cf..ec26409ca6a193125a7594a8f1808b1b5ea12487 100644 (file)
     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>
diff --git a/apps/workbench/app/views/pipeline_instances/_component_labels.html.erb b/apps/workbench/app/views/pipeline_instances/_component_labels.html.erb
new file mode 100644 (file)
index 0000000..d2d824b
--- /dev/null
@@ -0,0 +1,5 @@
+<% pipeline_jobs(object).each do |pj| %>
+  <span class="label label-<%= pj[:labeltype] %>">
+    <%= pj[:name] %>
+  </span>&nbsp;
+<% end %>
index 53cd6b3ddea37fe6c03660239d9f1fedd27d6c64..018384b37e8300996f181b748aeb8ae84ee3a17f 100644 (file)
   </div>
   <% @objects.each do |object| %>
   <div class="col-sm-<%= pi_span %>" style="overflow-x: hidden; text-overflow: ellipsis;">
-    <%= link_to_if_arvados_object object, friendly_name: true %>
+    <%= render :partial => "show_object_button", :locals => {object: object, size: 'sm' } %>
+    <%= object.name || "unnamed #{object.class_for_display.downcase}" %>
     <br />
-    Template: <%= link_to_if_arvados_object object.pipeline_template_uuid, friendly_name: true %>
+    <span class="deemphasize">Template:</span> <%= link_to_if_arvados_object object.pipeline_template_uuid, friendly_name: true %>
   </div>
   <% end %>
   </div>
index 099d4cd49cb877f4808aaab01e11137c2c135bbf..d7b86bf1fb3b930fdbc178329c0f8988d9d301d4 100644 (file)
-<% content_for :css do %>
-
-<% end %>
-
 <% template = PipelineTemplate.find(@object.pipeline_template_uuid) rescue nil %>
-
 <%= content_for :content_top do %>
   <h2>
-    <%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => 'Unnamed pipeline' } %>
+    <%= render_editable_attribute @object, 'name', nil %>
   </h2>
   <% if template %>
-  <h4>
-    From template:
-    <%= link_to_if_arvados_object template, friendly_name: true %>
-  </h4>
+  <blockquote><span class="deemphasize">From template:</span><br />
+    <%= link_to_if_arvados_object template, friendly_name: true %><br />
+    <%= template.description %>
+  </blockquote>
+  <% end %>
+<% end %>
+
+<% content_for :tab_line_buttons do %>
+  <%= link_to(copy_pipeline_instance_path('id' => @object.uuid, 'pipeline_instance[state]' => 'New'),
+      class: 'btn btn-primary',
+      #data: {toggle: :tooltip, placement: :top}, title: 'copy and modify',
+      method: :post,
+      ) do %>
+    Clone and edit <i class="fa fa-fw fa-copy"></i>
   <% end %>
 <% end %>
 
-<% pipeline_job_uuids = [] %>
+<% if !@object.state.in? ['New', 'Ready'] %>
 
-<% if !@object.state.in? ['New', 'Ready', 'Paused'] %>
-<table class="table pipeline-components-table">
-  <colgroup>
-    <col style="width: 15%" />
-    <col style="width: 20%" />
-    <col style="width: 12%" />
-    <col style="width: 12%" />
-    <col style="width: 45%" />
-  </colgroup>
-  <thead>
-    <tr>
-      <th>
-        component
-      </th><th>
-        script, version
-      </th><th>
-        job
-        <%# format:'js' here helps browsers avoid using the cached js
-        content in html context (e.g., duplicate tab -> see
-        javascript) %>
-        <%= link_to '(refresh)', {format: :js}, {class: 'refresh hide', remote: true, method: 'get'} %>
-      </th><th>
-      </th><th>
-        output
-      </th>
-    </tr>
-  </thead>
-  <tbody>
-    <% render_pipeline_jobs.each do |pj| %>
-      <% if pj[:job].andand[:uuid]
-           pipeline_job_uuids << pj[:job][:uuid]
-         end %>
-    <tr>
-      <td>
-        <%= pj[:name] %>
-      </td><td>
-        <%= pj[:script] %>
-        <br /><span class="deemphasize"><%= pj[:script_version] %></span>
-      </td><td>
-        <%= pj[:progress_bar] %>
-        <% if @object.state == 'Complete' || @object.state == 'Failed' %>
-          <% if pj[:job].andand[:uuid] %>
-            <span class="deemphasize">
-            <%= link_to("..."+pj[:job][:uuid].last(15), job_url(id: pj[:job][:uuid])) %>
-            </span>
+  <% content_for :tab_line_buttons do %>
+    <% if @object.state.in? ['RunningOnClient', 'RunningOnServer'] %>
+      <%= link_to(url_for('pipeline_instance[state]' => 'Paused'),
+          class: 'btn btn-primary run-pipeline-button',
+          method: :patch
+          ) do %>
+        Stop <i class="fa fa-fw fa-stop"></i>
+      <% end %>
+    <% elsif @object.state == 'Paused' %>
+      <%= link_to(url_for('pipeline_instance[state]' => 'RunningOnServer'),
+          class: 'btn btn-primary run-pipeline-button',
+          method: :patch
+          ) do %>
+        Resume <i class="fa fa-fw fa-play"></i>
+      <% end %>
+    <% end %>
+  <% end %>
 
-            <% current_job = Job.find(pj[:job][:uuid]) rescue nil %>
-            <% if current_job.andand[:log] %>
-              <% fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(current_job[:log])%>
-              <% Collection.limit(1).where(uuid: fixup[1]).each do |c| %>
-                <% c.files.each do |file| %>
-                  <br/><span class="deemphasize">
-                  <a href="<%= collection_path(current_job[:log]) %>/<%= file[1] %>?disposition=inline&size=<%= file[2] %>">log</a>
-                  </span>
+  <% pipeline_job_uuids = [] %>
+
+  <div class="pull-right">
+    Current state: <span class="badge badge-info"><%= @object.state.sub('OnServer', '') %></span>&nbsp;
+  </div>
+
+  <table class="table pipeline-components-table">
+    <colgroup>
+      <col style="width: 15%" />
+      <col style="width: 25%" />
+      <col style="width: 8%" />
+      <col style="width: 13%" />
+      <col style="width: 12%" />
+      <col style="width: 14%" />
+      <col style="width: 13%" />
+    </colgroup>
+    <thead>
+      <tr>
+        <th colspan="2">
+          component
+        </th><th colspan="5">
+          job
+          <%# format:'js' here helps browsers avoid using the cached js
+          content in html context (e.g., duplicate tab -> see
+          javascript) %>
+          <%= link_to '(refresh)', {format: :js}, {class: 'refresh hide', remote: true, method: 'get'} %>
+        </th>
+      </tr>
+    </thead>
+    <tbody>
+      <% render_pipeline_jobs.each do |pj| %>
+        <% if pj[:job].andand[:uuid]
+             pipeline_job_uuids << pj[:job][:uuid]
+           end %>
+      <tr>
+        <td>
+          <%= pj[:name] %>
+        </td><td>
+          <%= pj[:script] %>
+          <br /><span class="deemphasize"><%= pj[:script_version] %></span>
+        </td><td>
+          <%= render(partial: 'job_status_label', locals: { j: pj[:job] }) %>
+        </td><td>
+          <%= pj[:progress_bar] %>
+        </td>
+        <% current_job = Job.find(pj[:job][:uuid]) rescue nil %>
+        <td>
+          <% if current_job %>
+            <%= render partial: 'show_object_button', locals: {object: current_job, size: 'xs', link_text: 'Show job details'} %>
+          <% end %>
+        </td><td>
+          <% if current_job.andand[:log] %>
+            <% fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(current_job[:log])%>
+            <% Collection.limit(1).where(uuid: fixup[1]).each do |c| %>
+              <% c.files.first.andand do |file| %>
+                <%= link_to url_for(controller: 'collections', action: 'show_file', uuid: current_job[:log], file: "#{file[0]}/#{file[1]}", disposition: 'inline', size: file[2]), class: 'btn btn-default btn-xs' do %>
+                  <i class="fa fa-fw fa-info"></i> Show log messages
                 <% end %>
               <% end %>
             <% end %>
           <% end %>
-        <% end %>
-      </td><td>
-        <%= render(partial: 'job_status_label',
-                               locals: { :j => pj[:job] }) %>
-      </td><td>
-        <%= link_to_if_arvados_object pj[:output], {:thumbnail => true} %>
-      </td>
-    </tr>
-    <% end %>
-  </tbody>
-  <tfoot>
-    <tr><td colspan="5"></td></tr>
-  </tfoot>
-</table>
-
-<% if @object.state == 'RunningOnServer' || @object.state == 'RunningOnClient' %>
-
-<% content_for :tab_line_buttons do %>
-  <%= form_tag @object, :method => :put do |f| %>
+        </td><td>
+          <% if current_job.andand[:output] %>
+            <%= link_to_if_arvados_object current_job[:output], {thumbnail: true, link_text: raw('<i class="fa fa-fw fa-archive"></i> Show output files')}, {class: 'btn btn-default btn-xs'} %>
+          <% end %>
+        </td>
+      </tr>
+      <% end %>
+    </tbody>
+    <tfoot>
+      <tr><td colspan="7"></td></tr>
+    </tfoot>
+  </table>
 
-    <%= hidden_field @object.class.to_s.underscore.singularize.to_sym, :state, :value => 'Paused' %>
+  <% if @object.state.in? %w(RunningOnServer RunningOnClient) %>
 
-    <%= button_tag({class: 'btn btn-primary pull-right run-pipeline-button'}) do %>
-      Stop <i class="fa fa-fw fa-stop"></i>
+    <% content_for :js do %>
+      setInterval(function(){$('a.refresh').click()}, 15000);
     <% end %>
-  <% end %>
-<% end %>
 
     <% if !pipeline_job_uuids.empty? %>
       <h4>Log messages from running jobs</h4>
       <% log_history = pipeline_log_history(pipeline_job_uuids) %>
-      <div id="pipeline_event_log_history_div">
+      <div class="arv-log-event-listener arv-log-event-handler-append-logs arv-job-log-window" id="pipeline_event_log_div" data-object-uuids="<%=pipeline_job_uuids.join(" ")%>">
         <% log_history.each do |entry| %>
           <%=entry%><br/>
         <% end %>
       </div>
-      <div class="arv-log-event-listener arv-log-event-handler-append-logs" id="pipeline_event_log_div" data-object-uuids="<%=pipeline_job_uuids.join(" ")%>"/>
     <% end %>
-  <% end %>
 
-<% else %>    <%# State new or ready or paused %>
-  <% if @object.state == 'New' %>
-    <p>Please set the desired input parameters for the components of this pipeline.  Parameters highlighted in red are required.</p>
   <% end %>
 
-  <% content_for :tab_line_buttons do %>
-    <%= form_tag @object, :method => :put do |f| %>
+<% else %>
+  <%# state is either New or Ready %>
 
-      <%= hidden_field @object.class.to_s.underscore.singularize.to_sym, :state, :value => 'RunningOnServer' %>
+  <% if @object.state.in? %w(New Ready) %>
+    <p><i>Here are all of the pipeline's components (jobs that will need to run in order to complete the pipeline). If you know what you're doing (or you're experimenting) you can modify these parameters before starting the pipeline. Usually, you only need to edit the settings presented on the "Inputs" tab above.</i></p>
+  <% end %>
 
-      <%= button_tag({class: 'btn btn-primary pull-right run-pipeline-button'}) do %>
-        Run <i class="fa fa-fw fa-play"></i>
-      <% end %>
+  <% content_for :tab_line_buttons do %>
+    <%= link_to(url_for('pipeline_instance[state]' => 'RunningOnServer'),
+        class: 'btn btn-primary run-pipeline-button',
+        method: :patch
+        ) do %>
+      Run <i class="fa fa-fw fa-play"></i>
     <% end %>
   <% end %>
 
index 556e9f81ed3658fe14806d16310c4205d32f6e12..5106710b0eb1c580ca5f393f265b806f64c914c6 100644 (file)
@@ -8,7 +8,8 @@
       <% component[:script_parameters].andand.each do |pname, pvalue_spec| %>
         <% if pvalue_spec.is_a? Hash %>
           <% if (pvalue_spec[:description] or
-                 (pvalue_spec[:required] and not pvalue_spec[:value])) %>
+                 ((pvalue_spec[:required] or pvalue_spec[:optional] == false) and
+                  not pvalue_spec[:value])) %>
             <% n_inputs += 1 %>
             <label for="<% "#{cname}-#{pname}" %>">
               <%= pvalue_spec[:title] ||
 <% end %>
 
 <% if n_inputs == 0 %>
-  <p>This pipeline does not need any further inputs specified. You can start it by clicking the "Run" button.</p>
+  <p>This pipeline does not need any further inputs specified. You can start it by clicking the "Run" button whenever you're ready. (It's not too late to change existing settings, though.)</p>
 <% else %>
   <p><i>Provide <%= n_inputs > 1 ? 'values' : 'a value' %> for the following <%= n_inputs > 1 ? 'parameters' : 'parameter' %>, then click the "Run" button to start the pipeline.</i></p>
   <%= content_for :pi_input_form %>
 
-  <%= form_tag @object, :method => :put do |f| %>
-    <%= hidden_field @object.class.to_s.underscore.singularize.to_sym, :state, :value => 'RunningOnServer' %>
-    <%= button_tag({class: 'btn btn-primary run-pipeline-button'}) do %>
-      Run <i class="fa fa-fw fa-play"></i>
-    <% end %>
+  <%= link_to(url_for('pipeline_instance[state]' => 'RunningOnServer'),
+      class: 'btn btn-primary run-pipeline-button',
+      method: :patch
+      ) do %>
+    Run <i class="fa fa-fw fa-play"></i>
   <% end %>
 
 <% end %>
 
 <div style="margin-top: 1em;">
-  <p>Click the "Components" tab above to see a full list of pipeline components and parameters.</p>
+  <p>Click the "Components" tab above to see a full list of pipeline settings.</p>
 </div>
diff --git a/apps/workbench/app/views/pipeline_instances/_show_object_description_cell.html.erb b/apps/workbench/app/views/pipeline_instances/_show_object_description_cell.html.erb
new file mode 100644 (file)
index 0000000..38f51a3
--- /dev/null
@@ -0,0 +1,4 @@
+<div class="nowrap">
+  <%= object.content_summary %><br />
+  <%= render partial: 'pipeline_instances/component_labels', locals: {object: object} %>
+</div>
index e9a01dc253c1958e505195eba36e61d3aca75975..86eab623f6595508043ee31634580435056caceb 100644 (file)
 <% end %>
 
 <%= render partial: "paging", locals: {results: @objects, object: @object} %>
-
-<% content_for :footer_js do %>
-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 = $('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));
-            }
-        });
-    }
-};
-$('form input[name="uuids[]"]').on('click', showhide_compare);
-showhide_compare();
-<% end %>
index 9e8d1e566b2ea81699499ade0a93235f2b2919b3..99caf9bcb961cfae91169644ecf34be605363330 100644 (file)
@@ -1 +1,15 @@
+<% if (o = Group.find?(@objects.first.owner_uuid)) %>
+  <% content_for :breadcrumbs do %>
+    <li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
+    <li>
+      <%= link_to(o.name, project_path(o.uuid)) %>
+    </li>
+    <li class="nav-separator">
+      <span class="glyphicon glyphicon-arrow-right"></span>
+    </li>
+    <li>
+      <%= link_to '#' do %>compare pipelines<% end %>
+    </li>
+  <% end %>
+<% end %>
 <%= render partial: 'content', layout: 'content_layout', locals: {pane_list: controller.compare_pane_list }  %>
diff --git a/apps/workbench/app/views/pipeline_templates/_choose.js.erb b/apps/workbench/app/views/pipeline_templates/_choose.js.erb
new file mode 120000 (symlink)
index 0000000..8420a7f
--- /dev/null
@@ -0,0 +1 @@
+../application/_choose.js.erb
\ No newline at end of file
diff --git a/apps/workbench/app/views/pipeline_templates/_choose_rows.html.erb b/apps/workbench/app/views/pipeline_templates/_choose_rows.html.erb
new file mode 100644 (file)
index 0000000..6ffd6ae
--- /dev/null
@@ -0,0 +1,11 @@
+<% @objects.each do |object| %>
+  <div class="row filterable selectable <%= 'multiple' if multiple %>" data-object-uuid="<%= object.uuid %>">
+    <div class="col-sm-12" style="overflow-x:hidden">
+      <i class="fa fa-fw fa-gear"></i>
+      <%= object.name %>
+    </div>
+    <div class="col-sm-11 col-sm-push-1 arv-description-in-table">
+      <%= object.description %>
+    </div>
+  </div>
+<% end %>
similarity index 52%
rename from apps/workbench/app/views/folders/_choose.html.erb
rename to apps/workbench/app/views/projects/_choose.html.erb
index c27d669e2b65ea4c353c8e303c731b02345956ef..3481fddfd2e54a05ccc2fd30555d2c7b272bcd02 100644 (file)
@@ -4,25 +4,25 @@
 
       <div class="modal-header">
         <button type="button" class="close" onClick="reset_form()" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <h4 class="modal-title"><%= params[:title] || 'Choose folder' %></h4>
+        <h4 class="modal-title"><%= params[:title] || 'Choose project' %></h4>
       </div>
 
       <div class="modal-body">
-        <div class="container-fluid arv-folder-list selectable-container" style="height: 15em; overflow-y: scroll">
-          <% [@my_folder_tree, @shared_folder_tree].each do |tree| %>
-            <% tree.each do |foldernode| %>
-              <% if foldernode[:object].is_a? String %>
-                <div class="row" style="padding-left: <%= 1 + foldernode[:depth] %>em;">
+        <div class="selectable-container" style="height: 15em; overflow-y: scroll">
+          <% [@my_project_tree, @shared_project_tree].each do |tree| %>
+            <% tree.each do |projectnode| %>
+              <% if projectnode[:object].is_a? String %>
+                <div class="row" style="padding-left: <%= 1 + projectnode[:depth] %>em;">
                   <i class="fa fa-fw fa-folder-open-o"></i>
-                  <%= foldernode[:object] %>
+                  <%= projectnode[:object] %>
                 </div>
               <% else %>
-                <div class="<%= 'selectable folder' if !params[:editable] || foldernode[:object].editable? %> row" style="padding-left: <%= 1 + foldernode[:depth] %>em;" data-object-uuid="<%= foldernode[:object].uuid %>">
+                <div class="<%= 'selectable project' if !params[:editable] || projectnode[:object].editable? %> row" style="padding-left: <%= 1 + projectnode[:depth] %>em;" data-object-uuid="<%= projectnode[:object].uuid %>">
                   <i class="fa fa-fw fa-folder-o"></i>
-                  <% if foldernode[:object].uuid == current_user.uuid %>
-                    My Folders
+                  <% if projectnode[:object].uuid == current_user.uuid %>
+                    My Projects
                   <% else %>
-                    <%= foldernode[:object].friendly_link_name || 'New folder' %>
+                    <%= projectnode[:object].friendly_link_name || 'New project' %>
                   <% end %>
                 </div>
               <% end %>
diff --git a/apps/workbench/app/views/projects/_choose.js.erb b/apps/workbench/app/views/projects/_choose.js.erb
new file mode 120000 (symlink)
index 0000000..8420a7f
--- /dev/null
@@ -0,0 +1 @@
+../application/_choose.js.erb
\ No newline at end of file
diff --git a/apps/workbench/app/views/projects/_index_jobs_and_pipelines.html.erb b/apps/workbench/app/views/projects/_index_jobs_and_pipelines.html.erb
new file mode 100644 (file)
index 0000000..4c066f9
--- /dev/null
@@ -0,0 +1,26 @@
+<div>
+  <% any = false %>
+  <% recent_jobs_and_pipelines[0..9].each do |object| %>
+    <% any = true %>
+    <div class="row">
+      <div class="col-sm-4">
+        <%= render :partial => "show_object_button", :locals => {object: object, size: 'xs'} %>
+        <% if object.respond_to?(:name) %>
+          <%= render_editable_attribute object, 'name', nil, {tiptitle: 'rename', btnplacement: :left} %>
+        <% else %>
+          <%= object.class_for_display %> <%= object.uuid %>
+        <% end %>
+      </div>
+      <div class="col-sm-8 arv-description-in-table">
+        <%= render_controller_partial(
+            'show_object_description_cell.html',
+            controller_name: object.controller_name,
+            locals: {object: object})
+            %>
+      </div>
+    </div>
+  <% end %>
+  <% if not any %>
+    <span class="deemphasize">No jobs or pipelines to display.</span>
+  <% end %>
+</div>
diff --git a/apps/workbench/app/views/projects/_index_projects.html.erb b/apps/workbench/app/views/projects/_index_projects.html.erb
new file mode 100644 (file)
index 0000000..1e2d768
--- /dev/null
@@ -0,0 +1,31 @@
+<div class="container-fluid arv-project-list">
+  <% tree.each do |projectnode| %>
+    <% rowtype = projectnode[:object].class %>
+    <% next if rowtype != Group and !show_root_node %>
+    <div class="<%= 'project' if rowtype == Group %> row">
+      <div class="col-md-12" style="padding-left: <%= projectnode[:depth] - (show_root_node ? 0 : 1) %>em;">
+        <% if show_root_node and rowtype == String %>
+          <i class="fa fa-fw fa-folder-open-o"></i>
+          <%= projectnode[:object] %>
+        <% elsif show_root_node and rowtype == User %>
+          <% if projectnode[:object].uuid == current_user.andand.uuid %>
+            <i class="fa fa-fw fa-folder-open-o"></i>
+            My Projects
+          <% else %>
+            <i class="fa fa-fw fa-folder-o"></i>
+            <%= projectnode[:object].friendly_link_name %>
+          <% end %>
+        <% elsif rowtype == Group %>
+          <i class="fa fa-fw fa-folder-o"></i>
+          <% opts = {} %>
+          <% opts[:title] = projectnode[:object].description %>
+          <% opts[:'data-toggle'] = 'tooltip' %>
+          <% opts[:'data-placement'] = 'bottom' %>
+          <%= link_to projectnode[:object], opts do %>
+            <%= projectnode[:object].friendly_link_name %>
+          <% end %>
+        <% end %>
+      </div>
+    </div>
+  <% end %>
+</div>
diff --git a/apps/workbench/app/views/projects/_show_contents.html.erb b/apps/workbench/app/views/projects/_show_contents.html.erb
new file mode 100644 (file)
index 0000000..5132796
--- /dev/null
@@ -0,0 +1,109 @@
+<% content_for :content_top do %>
+
+<h2>
+  <%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => "New project" } %>
+</h2>
+
+<div class="arv-description-as-subtitle">
+  <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "(No description provided)", 'data-toggle' => 'manual' } %>
+</div>
+
+<% end %>
+
+<% content_for :tab_line_buttons do %>
+  <% if @objects_and_names.empty? %>
+    <%= button_to(project_path(id: @object.uuid, return_to: projects_path), method: 'delete', class: 'btn btn-sm btn-primary', data: {confirm: "Really delete project '#{@object.name}'?"}) do %>
+      <i class="fa fa-fw fa-trash-o"></i> Delete project
+    <% end %>
+  <% end %>
+<% end %>
+
+<div class="selection-action-container">
+  <div class="row">
+    <div class="col-sm-5">
+      <div class="btn-group btn-group-sm">
+        <button type="button" class="btn btn-default">Selection...</button>
+        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+          <span class="caret"></span>
+          <span class="sr-only">Toggle Dropdown</span>
+        </button>
+        <ul class="dropdown-menu" role="menu">
+          <li><%= link_to "Compare selected", '#',
+                  'data-href' => compare_pipeline_instances_path,
+                  'data-selection-param-name' => 'uuids[]',
+                  'data-selection-action' => 'compare'
+            %></li>
+          <li><%= link_to "Remove selected", '#',
+                  'data-href' => url_for(action: :remove_items),
+                  'data-selection-param-name' => 'item_uuids[]',
+                  'data-selection-action' => 'remove',
+                  'data-remote' => true,
+                  'method' => 'delete'
+            %></li>
+        </ul>
+      </div>
+      <% if @object.editable? %>
+        <%= link_to(
+              choose_collections_path(
+                title: 'Add data to project:',
+                multiple: true,
+                action_name: 'Add',
+                action_href: actions_path(id: @object.uuid),
+                action_method: 'post',
+                action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
+              { class: "btn btn-primary btn-sm", remote: true, method: 'get', data: {'event-after-select' => 'page-refresh'} }) do %>
+          <i class="fa fa-fw fa-plus"></i> Add data
+        <% end %>
+        <%= link_to(
+              choose_pipeline_templates_path(
+                title: 'Choose a pipeline to run:',
+                action_name: 'Next: choose inputs <i class="fa fa-fw fa-arrow-circle-right"></i>',
+                action_href: pipeline_instances_path,
+                action_method: 'post',
+                action_data: {'selection_param' => 'pipeline_instance[pipeline_template_uuid]', 'pipeline_instance[owner_uuid]' => @object.uuid, 'success' => 'redirect-to-created-object'}.to_json),
+              { class: "btn btn-primary btn-sm", remote: true, method: 'get' }) do %>
+          <i class="fa fa-fw fa-gear"></i> Run a pipeline
+        <% end %>
+      <% end %>
+    </div>
+    <div class="col-sm-3">
+      <form class="form-inline" role="form">
+        Show:
+        <select class="form-control form-control-sm filterable-control" data-filterable-attribute="data-kind" data-filterable-target="table.arv-index.arv-project-contents tbody">
+          <option value="">Everything</option>
+          <option value="arvados#collection">Data</option>
+          <option value="arvados#pipelineInstance arvados#job">Compute jobs</option>
+          <option value="arvados#pipelineTemplate">Pipelines</option>
+          <!--
+          <option value="arvados#specimen">Specimens</option>
+          <option value="arvados#human">Humans</option>
+          <option value="arvados#trait">Traits</option>
+          -->
+        </select>
+      </form>
+    </div>
+    <div class="col-sm-4">
+      <input type="text" class="form-control filterable-control" placeholder="Search project contents" data-filterable-target="table.arv-index.arv-project-contents tbody"/>
+    </div>
+  </div>
+
+  <table class="table table-condensed table-fixedlayout arv-index arv-project-contents" style="overflow-x: hidden">
+    <colgroup>
+      <col width="40%" />
+      <col width="60%" />
+    </colgroup>
+    <tbody data-infinite-scroller="#Contents-scroll" data-infinite-content-href="<%= url_for(format: :json, partial: :contents_rows, offset: next_page_offset) if next_page_offset %>">
+      <%= render partial: 'show_contents_rows', locals: {project: @object, objects_and_names: @objects_and_names} %>
+    </tbody>
+    <thead>
+      <tr>
+        <th>
+        </th>
+        <th>
+          description
+        </th>
+      </tr>
+    </thead>
+  </table>
+
+</div>
diff --git a/apps/workbench/app/views/projects/_show_contents_rows.html.erb b/apps/workbench/app/views/projects/_show_contents_rows.html.erb
new file mode 100644 (file)
index 0000000..8b19c00
--- /dev/null
@@ -0,0 +1,30 @@
+<% objects_and_names.each do |object, name_link| %>
+  <% name_object = (object.respond_to?(:name) || !name_link) ? object : name_link %>
+  <tr class="filterable"
+      data-object-uuid="<%= name_object.uuid %>"
+      data-kind="<%= object.kind %>"
+      >
+    <td>
+      <%= render partial: 'selection_checkbox', locals: {object: object, friendly_name: ((name_object.name rescue '') || '')} %>
+
+      <% if project.editable? %>
+        <%= link_to({action: 'remove_item', id: project.uuid, item_uuid: ((name_link && name_link.uuid) || object.uuid)}, method: :delete, remote: true, data: {confirm: "Remove #{object.class_for_display.downcase} #{name_object.name rescue object.uuid} from this project?", toggle: 'tooltip', placement: 'top'}, class: 'btn btn-sm btn-default btn-nodecorate', title: 'remove') do %>
+          <i class="fa fa-fw fa-ban"></i>
+        <% end %>
+      <% else %>
+        <i class="fa fa-fw"></i><%# placeholder %>
+      <% end %>
+
+      <%= render :partial => "show_object_button", :locals => {object: object, size: 'sm', name_link: name_link} %>
+
+      <%= render_editable_attribute (name_link || object), 'name', nil, {btnplacement: :left, tiptitle: 'rename'} %>
+    </td>
+    <td class="arv-description-in-table">
+      <%= render_controller_partial(
+          'show_object_description_cell.html',
+          controller_name: object.controller_name,
+          locals: {object: object})
+          %>
+    </td>
+  </tr>
+<% end %>
diff --git a/apps/workbench/app/views/projects/_show_featured.html.erb b/apps/workbench/app/views/projects/_show_featured.html.erb
new file mode 100644 (file)
index 0000000..6547b5e
--- /dev/null
@@ -0,0 +1,18 @@
+<div class="row">
+  <% @objects[0..3].each do |object| %>
+  <div class="card arvados-object">
+    <div class="card-top blue">
+      <a href="#">
+        <img src="/favicon.ico" alt=""/>
+      </a>
+    </div>
+    <div class="card-info">
+      <span class="title"><%= @objects.name_for(object) || object.class_for_display %></span>
+      <div class="desc"><%= object.respond_to?(:description) ? object.description : object.uuid %></div>
+    </div>
+    <div class="card-bottom">
+      <%= render :partial => "show_object_button", :locals => {object: object, htmloptions: {class: 'btn-default btn-block'}} %>
+    </div>
+  </div>
+  <% end %>
+</div>
diff --git a/apps/workbench/app/views/projects/_show_permissions.html.erb b/apps/workbench/app/views/projects/_show_permissions.html.erb
new file mode 100644 (file)
index 0000000..9d93ce0
--- /dev/null
@@ -0,0 +1,53 @@
+<div>
+  <div class="row">
+    <div class="col-md-6">
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          Additional permissions
+        </div>
+        <div class="panel-body">
+          <p>
+            <% if not @share_links.any? %>
+              <span class="deemphasize">(No additional permissions)</span>
+            <% else %>
+              Also shared with:
+              <% @share_links.andand.each do |link| %>
+                <br /><%= link_to_if_arvados_object link.tail_uuid, friendly_name: true %>
+              <% end %>
+            <% end %>
+          </p>
+        </div>
+      </div>
+    </div>
+    <div class="col-md-6">
+      <% if @object.owner %>
+        <div class="panel panel-default">
+          <div class="panel-heading">
+            Inherited permissions
+          </div>
+          <div class="panel-body">
+            <p>Permissions for this project are inherited by the owner or parent project:
+            </p>
+            <p style="margin-left: 4em">
+              <% if User == resource_class_for_uuid(@object.owner_uuid) %>
+                <i class="fa fa-fw fa-user"></i>
+              <% else %>
+                <i class="fa fa-fw fa-folder"></i>
+              <% end %>
+              <%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %>
+              <%= button_to('Move to...',
+                  choose_projects_path(
+                   title: 'Move to...',
+                   editable: true,
+                   action_name: 'Move',
+                   action_href: project_path(@object.uuid),
+                   action_method: 'put',
+                   action_data: {selection_param: 'project[owner_uuid]', success: 'page-refresh'}.to_json),
+                  { class: "btn btn-default btn-xs arv-move-to-project", remote: true, method: 'get' }) %>
+            </p>
+          </div>
+        </div>
+      <% end %>
+    </div>
+  </div>
+</div>
diff --git a/apps/workbench/app/views/projects/index.html.erb b/apps/workbench/app/views/projects/index.html.erb
new file mode 100644 (file)
index 0000000..6d79bc4
--- /dev/null
@@ -0,0 +1,79 @@
+<% content_for :breadcrumbs do %>
+<li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
+<li><a href="#">Home</a></li>
+<% end %>
+
+<div>
+  <div class="row">
+    <div class="col-sm-6">
+      <% if my_projects.empty? %>
+        <div class="panel panel-info">
+          <div class="panel-heading">
+            <h3 class="panel-title">
+              Welcome to <b><%= Rails.configuration.site_name %></b>.
+            </h3>
+          </div>
+          <div class="panel-body">
+            <img src="/favicon.ico" class="pull-right" alt="" style="opacity: 0.3"/>
+            <p>
+              This site runs Arvados, the open source biomedical analysis platform. <a href="https://arvados.org" target="_blank">Learn more&hellip;</a>
+            </p>
+            <p>
+              <b>To get started,</b> create a project using the "Add new project" button below.
+            </p>
+          </div>
+        </div>
+      <% end %>
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <div class="pull-right">
+            <%= button_to projects_path(method: 'post'), class: 'btn btn-xs btn-primary' do %>
+              <i class="fa fa-fw fa-plus"></i>
+              Add new project
+            <% end %>
+          </div>
+          <h3 class="panel-title">
+            My projects
+          </h3>
+        </div>
+        <div class="panel-body scroll-20em">
+          <%= render partial: 'index_projects', locals: {tree: @my_project_tree, show_root_node: false} %>
+        </div>
+      </div>
+    </div>
+    <div class="col-sm-6">
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <h3 class="panel-title">
+            Shared projects
+          </h3>
+        </div>
+        <div class="panel-body scroll-20em">
+          <%= render partial: 'index_projects', locals: {tree: @shared_project_tree, show_root_node: false} %>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-sm-12">
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <div class="pull-right">
+            <%= link_to jobs_path, class: 'btn btn-default btn-xs' do %>
+              All jobs <i class="fa fa-fw fa-arrow-circle-right"></i>
+            <% end %>
+            <%= link_to pipeline_instances_path, class: 'btn btn-default btn-xs' do %>
+              All pipelines <i class="fa fa-fw fa-arrow-circle-right"></i>
+            <% end %>
+          </div>
+          <h3 class="panel-title">
+            Recent jobs and pipelines
+          </h3>
+        </div>
+        <div class="panel-body">
+          <%= render partial: 'index_jobs_and_pipelines' %>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
index 5e8b3f8a9440213dbc55e1b68146af1a4cccfe0e..0c5739c15873b13130307818dac9e7bd5f2befce 100644 (file)
@@ -27,7 +27,7 @@
       }
 <% end %>
 
-<div class="container-fluid" id="home-tables">
+<div id="home-tables">
 
     <%= render :partial => 'tables' %>
 
diff --git a/apps/workbench/app/views/users/inactive.html.erb b/apps/workbench/app/views/users/inactive.html.erb
new file mode 100644 (file)
index 0000000..5f825e4
--- /dev/null
@@ -0,0 +1,22 @@
+<% content_for :breadcrumbs do raw '<!-- -->' end %>
+
+<div class="row">
+  <div class="col-sm-8 col-sm-push-4" style="margin-top: 1em">
+    <div class="well clearfix">
+      <%= image_tag "dax.png", style: "width: 147px; height: 197px; max-width: 25%; margin-right: 2em", class: 'pull-left' %>
+
+      <h3>Hi! You're logged in, but...</h3>
+
+      <p>
+
+        Your account is inactive.
+
+      </p><p>
+
+        An administrator must activate your account before you can get
+        any further.
+
+      </p>
+    </div>
+  </div>
+</div>
index 537041e8f8871c45749db7a3e04c2b5f4520f333..9cacebda02971ccdbff2d28569dd7a6d55e35cff 100644 (file)
@@ -1,23 +1,38 @@
 <% content_for :breadcrumbs do raw '<!-- -->' end %>
 
-<%= image_tag "dax.png", style: "float: left; max-width: 25%; margin-right: 2em" %>
-<h1>Hi there!  Please log in to use <%= Rails.configuration.site_name %>.</h1>
-<div class="row-fluid">
-  <div class="col span8" style="margin-top: 1em">
+<div class="row">
+  <div class="col-sm-8 col-sm-push-4" style="margin-top: 1em">
     <div class="well clearfix">
-      <p>When you click on the button below you will be taken to a Google sign-in page.
-       After entering your information, you will be redirected back to <%= Rails.configuration.site_name %>
-       If you have never used <%= Rails.configuration.site_name %> before, logging in for the first
-       time will also create a new user account. <%= Rails.configuration.site_name %> uses your name and
-       email address from Google services only for identification, and can not access any personal information
-  beyond that.
-  </p>
+      <%= image_tag "dax.png", style: "width: 147px; height: 197px; max-width: 25%; margin-right: 2em", class: 'pull-left' %>
+
+      <h3>Please log in.</h3>
+
+      <p>
+
+        The "Log in" button below will show you a Google sign-in page.
+       After you assure Google that you want to log in here with your
+       Google account, you will be redirected back here to
+       <%= Rails.configuration.site_name %>.
+
+      </p><p>
+
+       If you have never used <%= Rails.configuration.site_name %>
+       before, logging in for the first time will automatically
+       create a new account.
+
+      </p><p>
+
+        <i><%= Rails.configuration.site_name %> uses your name and
+       email address only for identification, and does not retrieve
+       any other personal information from Google.</i>
+
+      </p>
       <p>
-       <a  class="pull-right btn btn-primary" href="<%= arvados_api_client.arvados_login_url(return_to: request.url) %>">
-         Click here to log in to <%= Rails.configuration.site_name %> with a Google account</a>
+       <%= link_to arvados_api_client.arvados_login_url(return_to: request.url), class: "pull-right btn btn-primary" do %>
+          Log in to <%= Rails.configuration.site_name %>
+          <i class="fa fa-fw fa-arrow-circle-right"></i>
+        <% end %>
       </p>
     </div>
   </div>
 </div>
-
-
index 383d4421e2eb0e84e7cd1d14b94ea1e51e739e2a..a4f69b36b00eb07c577bcc5d7bfb02f83cf2fa89 100644 (file)
@@ -37,9 +37,12 @@ ArvadosWorkbench::Application.routes.draw 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'
@@ -48,20 +51,22 @@ ArvadosWorkbench::Application.routes.draw do
     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 19d08d73b35896b8116eb42c7d8dfecf42294177..fb80f8874ec0346d735e2c0c31b400fc04696423 100644 (file)
@@ -53,11 +53,11 @@ class CollectionsControllerTest < ActionController::TestCase
     assert_equal([['.', 'foo', 3]], assigns(:object).files)
   end
 
-  test "viewing a collection fetches related folders" do
+  test "viewing a collection fetches related projects" do
     show_collection(:foo_file, :active)
-    assert_includes(assigns(:folders).map(&:uuid),
-                    api_fixture('groups')['afolder']['uuid'],
-                    "controller did not find linked folder")
+    assert_includes(assigns(:projects).map(&:uuid),
+                    api_fixture('groups')['aproject']['uuid'],
+                    "controller did not find linked project")
   end
 
   test "viewing a collection fetches related permissions" do
@@ -153,4 +153,16 @@ class CollectionsControllerTest < ActionController::TestCase
     assert_not_equal(read_token, session[:arvados_api_token],
                      "using a reader token set the session's API token")
   end
+
+  test "inactive user can retrieve user agreement" do
+    ua_collection = api_fixture('collections')['user_agreement']
+    get :show_file, {
+      uuid: ua_collection['uuid'],
+      file: ua_collection['manifest_text'].match(/ \d+:\d+:(\S+)/)[1]
+    }, session_for(:inactive)
+    assert_nil(assigns(:required_user_agreements),
+               "Did not skip check_user_agreements filter " +
+               "when showing the user agreement.")
+    assert_response :success
+  end
 end
diff --git a/apps/workbench/test/functional/folders_controller_test.rb b/apps/workbench/test/functional/folders_controller_test.rb
deleted file mode 100644 (file)
index 2e06cca..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class FoldersControllerTest < ActionController::TestCase
-  # test "the truth" do
-  #   assert true
-  # end
-end
diff --git a/apps/workbench/test/functional/projects_controller_test.rb b/apps/workbench/test/functional/projects_controller_test.rb
new file mode 100644 (file)
index 0000000..b338aa8
--- /dev/null
@@ -0,0 +1,12 @@
+require 'test_helper'
+
+class ProjectsControllerTest < ActionController::TestCase
+  test "inactive user is asked to sign user agreements on front page" do
+    get :index, {}, session_for(:inactive)
+    assert_response :success
+    assert_not_empty assigns(:required_user_agreements),
+    "Inactive user did not have required_user_agreements"
+    assert_template 'user_agreements/index',
+    "Inactive user was not presented with a user agreement at the front page"
+  end
+end
index 8b026cb14f0dee989dd750bbac644a4191e90fa6..bf21a26435583dbb15d3097528e83003335e67c3 100644 (file)
@@ -8,7 +8,8 @@ class UsersControllerTest < ActionController::TestCase
 
   test "ignore previously valid token (for deleted user), don't crash" do
     get :welcome, {}, session_for(:valid_token_deleted_user)
-    assert_response :success
+    assert_response :redirect
+    assert_match /^#{Rails.configuration.arvados_login_base}/, @response.redirect_url
     assert_nil assigns(:my_jobs)
     assert_nil assigns(:my_ssh_keys)
   end
index 911daa02dc9645c882b52c48b125dbe5b2a86b19..8978f73a5434d8da14904bab0b774c83a7768e21 100644 (file)
@@ -13,7 +13,7 @@ class CollectionsTest < ActionDispatch::IntegrationTest
     page.assert_no_selector "div[data-persistent-state='#{oldstate}']"
   end
 
-  ['/collections', '/'].each do |path|
+  ['/collections', '/users/welcome'].each do |path|
     test "Flip persistent switch at #{path}" do
       Capybara.current_driver = Capybara.javascript_driver
       uuid = api_fixture('collections')['foo_file']['uuid']
@@ -40,7 +40,7 @@ class CollectionsTest < ActionDispatch::IntegrationTest
 
   test "Collection page renders default name links" do
     uuid = api_fixture('collections')['foo_file']['uuid']
-    coll_name = api_fixture('links')['foo_collection_name_in_afolder']['name']
+    coll_name = api_fixture('links')['foo_collection_name_in_aproject']['name']
     visit page_with_token('active', "/collections/#{uuid}")
     assert(page.has_text?(coll_name), "Collection page did not include name")
     # Now check that the page is otherwise normal, and the collection name
diff --git a/apps/workbench/test/integration/folders_test.rb b/apps/workbench/test/integration/folders_test.rb
deleted file mode 100644 (file)
index 7518d3c..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
-
-class FoldersTest < ActionDispatch::IntegrationTest
-  setup do
-    Capybara.current_driver = Capybara.javascript_driver
-  end
-
-  test 'Find a folder and edit its description' do
-    visit page_with_token 'active', '/'
-    find('nav a', text: 'Folders').click
-    find('.arv-folder-list a,button', text: 'A Folder').
-      click
-    within('.panel', text: api_fixture('groups')['afolder']['name']) do
-      find('span', text: api_fixture('groups')['afolder']['name']).click
-      find('.glyphicon-ok').click
-      find('.btn', text: 'Edit description').click
-      find('.editable-input textarea').set('I just edited this.')
-      find('.editable-submit').click
-      wait_for_ajax
-    end
-    visit current_path
-    assert(find?('.panel', text: 'I just edited this.'),
-           "Description update did not survive page refresh")
-  end
-
-  test 'Add a new name, then edit it, without creating a duplicate' do
-    folder_uuid = api_fixture('groups')['afolder']['uuid']
-    specimen_uuid = api_fixture('specimens')['owned_by_afolder_with_no_name_link']['uuid']
-    visit page_with_token 'active', '/folders/' + folder_uuid
-    within('.panel tr', text: specimen_uuid) do
-      find(".editable[data-name='name']").click
-      find('.editable-input input').set('Now I have a name.')
-      find('.glyphicon-ok').click
-      find('.editable', text: 'Now I have a name.').click
-      find('.editable-input input').set('Now I have a new name.')
-      find('.glyphicon-ok').click
-      wait_for_ajax
-      find('.editable', text: 'Now I have a new name.')
-    end
-    visit current_path
-    within '.panel', text: 'Contents' do
-      find '.editable', text: 'Now I have a new name.'
-      page.assert_no_selector '.editable', text: 'Now I have a name.'
-    end
-  end
-
-  test 'Create a folder and move it into a different folder' do
-    visit page_with_token 'active', '/folders'
-    find('input[value="Add a new folder"]').click
-
-    within('.panel', text: 'New folder') do
-      find('.panel-title span', text: 'New folder').click
-      find('.editable-input input').set('Folder 1234')
-      find('.glyphicon-ok').click
-    end
-    wait_for_ajax
-
-    visit '/folders'
-    find('input[value="Add a new folder"]').click
-    within('.panel', text: 'New folder') do
-      find('.panel-title span', text: 'New folder').click
-      find('.editable-input input').set('Folder 5678')
-      find('.glyphicon-ok').click
-    end
-    wait_for_ajax
-
-    find('input[value="Move to..."]').click
-    find('.selectable', text: 'Folder 1234').click
-    find('a,button', text: 'Move').click
-    wait_for_ajax
-
-    # Wait for the page to refresh and show the new parent folder in
-    # the Permissions panel:
-    find('.panel', text: 'Folder 1234')
-
-    assert(find('.panel', text: 'Permissions inherited from').
-           all('*', text: 'Folder 1234').any?,
-           "Folder 5678 should now be inside folder 1234")
-  end
-
-end
index 6e5389e7cc11d0a4d6c7c4808023476346e9c978..be7e4e15a5692d5b47ce433fb5647cbb7159bd29 100644 (file)
@@ -1,4 +1,4 @@
-require 'test_helper'
+require 'integration_helper'
 
 class LoginsTest < ActionDispatch::IntegrationTest
   test "login with api_token works after redirect" do
@@ -13,10 +13,9 @@ class LoginsTest < ActionDispatch::IntegrationTest
   end
 
   test "expired token yields login page, not error page" do
-    skip
     visit page_with_token('expired_trustedclient')
     # Even the error page has a "Log in" link. We should look for
     # something that only appears the real login page.
-    assert page.has_text? 'Please log in'
+    assert page.has_text? ' Log in Oh... fiddlesticks. Sorry, I had some trouble handling your request'
   end
 end
index b676dc74fc316da7f3c6cd2d15d64f09b56261e2..f0620cb7e8ac7f1e7e0e9911e884b6c536498d2d 100644 (file)
@@ -13,32 +13,54 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
   test 'Create and run a pipeline' do
     visit page_with_token('active_trustedclient')
 
-    click_link 'Pipeline templates'
+    visit '/pipeline_templates'
     within('tr', text: 'Two Part Pipeline Template') do
       find('a,button', text: 'Run').click
     end
 
+    # This pipeline needs input. So, Run should be disabled
+    page.assert_selector 'a.disabled,button.disabled', text: 'Run'
+
     instance_page = current_path
 
+    # put this pipeline instance in "A Project"
+    find('button', text: 'Choose a project...').click
+    within('.modal-dialog') do
+      find('.selectable', text: 'A Project').click
+      find('button', text: 'Move').click
+    end
+
     # Go over to the collections page and select something
-    click_link 'Collections (data files)'
+    visit '/collections'
     within('tr', text: 'GNU_General_Public_License') do
       find('input[type=checkbox]').click
     end
     find('#persistent-selection-count').click
 
-    # Go back to the pipeline instance page to use the new selection
-    visit instance_page
+    # Add this collection to the project
+    visit '/projects'
+    find('.arv-project-list a,button', text: 'A Project').click
+    find('.btn', text: 'Add data').click
+    find('span', text: 'foo_tag').click
+    within('.modal-dialog') do
+      find('.btn', text: 'Add').click
+    end
+   
+    find('tr[data-kind="arvados#pipelineInstance"]', text: 'New pipeline instance').
+      find('a', text: 'Show').
+      click
 
-    page.assert_selector 'a.disabled,button.disabled', text: 'Run'
     assert find('p', text: 'Provide a value')
 
     find('div.form-group', text: 'Foo/bar pair').
-      find('a,input').
+      find('.btn', text: 'Choose').
       click
-    find('.editable-input select').click
-    find('.editable-input').
-      first(:option, 'b519d9cb706a29fc7ea24dbea2f05851+249025').click
+
+    within('.modal-dialog') do
+      find('span', text: 'foo_tag').click
+      find('button', text: 'OK').click
+    end
+
     wait_for_ajax
 
     # "Run" button is now enabled
@@ -50,11 +72,78 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
     page.assert_selector 'a,button', text: 'Stop'
     find('a,button', text: 'Stop').click
 
-    # Pipeline is stopped. We have the option to resume it.
-    page.assert_selector 'a,button', text: 'Run'
+    # Pipeline is stopped. It should now be in paused state and Runnable again.
+    assert page.has_text? 'Paused'
+    page.assert_no_selector 'a.disabled,button.disabled', text: 'Resume'
+    page.assert_selector 'a,button', text: 'Clone and edit'
+
+    # Since it is test env, no jobs are created to run. So, graph not visible
+    assert_not page.has_text? 'Graph'
+  end
+
+  # Create a pipeline instance from within a project and run
+  test 'Create pipeline inside a project and run' do
+    visit page_with_token('active_trustedclient')
+
+    # Go over to the collections page and select something
+    visit '/collections'
+    within('tr', text: 'GNU_General_Public_License') do
+      find('input[type=checkbox]').click
+    end
+    find('#persistent-selection-count').click
+
+    # Add this collection to the project using collections menu from top nav
+    visit '/projects'
+    find('.arv-project-list a,button', text: 'A Project').click
+
+    find('#collections-menu').click
+    click_button 'Copy selections into this project'
+
+    # create a pipeline instance
+    find('.btn', text: 'Run a pipeline').click
+    within('.modal-dialog') do
+      assert page.has_text? 'Two Part Pipeline Template'
+      find('.fa-gear').click
+      find('.btn', text: 'Next: choose inputs').click
+    end
+
+    assert find('p', text: 'Provide a value')
+
+    find('div.form-group', text: 'Foo/bar pair').
+      find('.btn', text: 'Choose').
+      click
 
-    # Go over to the graph tab
+    within('.modal-dialog') do
+      find('span', text: 'foo_tag').click
+      find('button', text: 'OK').click
+    end
+
+    wait_for_ajax
+
+    # "Run" button present and enabled
+    page.assert_no_selector 'a.disabled,button.disabled', text: 'Run'
+    first('a,button', text: 'Run').click
+
+    # Pipeline is running. We have a "Stop" button instead now.
+    page.assert_no_selector 'a,button', text: 'Run'
+    page.assert_selector 'a,button', text: 'Stop'
+
+    # Since it is test env, no jobs are created to run. So, graph not visible
+    assert_not page.has_text? 'Graph'
+  end
+
+  test 'view pipeline with job and see graph' do
+    visit page_with_token('active_trustedclient')
+
+    visit '/pipeline_instances'
+    assert page.has_text? 'pipeline_with_job'
+
+    find('a', text: 'pipeline_with_job').click
+
+    # since the pipeline component has a job, expect to see the graph
+    assert page.has_text? 'Graph'
     click_link 'Graph'
-    assert page.has_css? 'div#provenance_graph'
+    assert page.has_text? 'script_version'
   end
+
 end
diff --git a/apps/workbench/test/integration/projects_test.rb b/apps/workbench/test/integration/projects_test.rb
new file mode 100644 (file)
index 0000000..73733d2
--- /dev/null
@@ -0,0 +1,88 @@
+require 'integration_helper'
+require 'selenium-webdriver'
+require 'headless'
+
+class ProjectsTest < ActionDispatch::IntegrationTest
+  setup do
+    Capybara.current_driver = Capybara.javascript_driver
+  end
+
+  test 'Find a project and edit its description' do
+    visit page_with_token 'active', '/'
+    find('.arv-project-list a,button', text: 'A Project').
+      click
+    within('.container-fluid', text: api_fixture('groups')['aproject']['name']) do
+      find('span', text: api_fixture('groups')['aproject']['name']).click
+      within('.arv-description-as-subtitle') do
+        find('.fa-pencil').click
+        find('.editable-input textarea').set('I just edited this.')
+        find('.editable-submit').click
+      end
+      wait_for_ajax
+    end
+    visit current_path
+    assert(find?('.container-fluid', text: 'I just edited this.'),
+           "Description update did not survive page refresh")
+  end
+
+  test 'Add a new name, then edit it, without creating a duplicate' do
+    project_uuid = api_fixture('groups')['aproject']['uuid']
+    specimen_uuid = api_fixture('specimens')['owned_by_aproject_with_no_name_link']['uuid']
+    visit page_with_token 'active', '/projects/' + project_uuid
+    within(".selection-action-container") do
+      within (first('tr', text: 'Specimen')) do
+        find(".fa-pencil").click
+        find('.editable-input input').set('Now I have a name.')
+        find('.glyphicon-ok').click
+        find('.editable', text: 'Now I have a name.').click
+        find(".fa-pencil").click
+        find('.editable-input input').set('Now I have a new name.')
+        find('.glyphicon-ok').click
+        end
+      wait_for_ajax
+      find('.editable', text: 'Now I have a new name.')
+    end
+    visit current_path
+    within '.selection-action-container' do
+      find '.editable', text: 'Now I have a new name.'
+      page.assert_no_selector '.editable', text: 'Now I have a name.'
+    end
+  end
+
+  test 'Create a project and move it into a different project' do
+    visit page_with_token 'active', '/projects'
+    find('.btn', text: "Add new project").click
+
+    # within('.editable', text: 'New project') do
+    within('h2') do
+      find('.fa-pencil').click
+      find('.editable-input input').set('Project 1234')
+      find('.glyphicon-ok').click
+    end
+    wait_for_ajax
+
+    visit '/projects'
+    find('.btn', text: "Add new project").click
+    within('h2') do
+      find('.fa-pencil').click
+      find('.editable-input input').set('Project 5678')
+      find('.glyphicon-ok').click
+    end
+    wait_for_ajax
+
+    click_link 'Permissions'
+    find('input[value="Move to..."]').click
+    find('.selectable', text: 'Project 1234').click
+    find('a,button', text: 'Move').click
+    wait_for_ajax
+
+    # Wait for the page to refresh and show the new parent in Permissions panel
+    click_link 'Permissions'
+    find('.panel', text: 'Project 1234')
+
+    assert(find('.panel', text: 'Permissions for this project are inherited by the owner or parent project').
+           all('*', text: 'Project 1234').any?,
+           "Project 5678 should now be inside project 1234")
+  end
+
+end
index 729345673e74c9939d2e78a3460454c4519980bf..efaea00b01c42ff4a06734e07be8505cf28125f4 100644 (file)
@@ -13,7 +13,7 @@ class SmokeTest < ActionDispatch::IntegrationTest
   end
 
   def all_links_in(find_spec, text_regexp=//)
-    find(find_spec).all('a').collect { |tag|
+    all(find_spec + ' a').collect { |tag|
       if tag[:href].nil? or tag[:href].empty? or (tag.text !~ text_regexp)
         nil
       else
@@ -26,7 +26,7 @@ class SmokeTest < ActionDispatch::IntegrationTest
   test "all first-level links succeed" do
     visit page_with_token('active_trustedclient', '/')
     assert_visit_success
-    click_link 'user-menu'
+    click_link 'notifications-menu'
     urls = [all_links_in('nav'),
             all_links_in('.navbar', /^Manage /)].flatten
     seen_urls = ['/']
index 6646e5a3f1df27bb0d4fbd141ca2c67e27c25155..dc3957c7b19ece6508334a518002377c3b674d72 100644 (file)
@@ -16,6 +16,7 @@ class UsersTest < ActionDispatch::IntegrationTest
     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
@@ -24,10 +25,10 @@ class UsersTest < ActionDispatch::IntegrationTest
     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
@@ -50,6 +51,7 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     visit page_with_token('admin_trustedclient')
 
+    find('#system-menu').click
     click_link 'Users'
 
     assert page.has_text? 'zzzzz-tpzed-d9tiejq69daie8f'
@@ -70,15 +72,13 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     # 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'
@@ -86,9 +86,10 @@ class UsersTest < ActionDispatch::IntegrationTest
       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
@@ -100,11 +101,12 @@ class UsersTest < ActionDispatch::IntegrationTest
     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
@@ -124,9 +126,10 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     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'
@@ -142,9 +145,10 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     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
@@ -157,11 +161,12 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     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
@@ -187,10 +192,11 @@ class UsersTest < ActionDispatch::IntegrationTest
       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'
@@ -206,9 +212,10 @@ class UsersTest < ActionDispatch::IntegrationTest
 
     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
index 26da0d0b039efa78e03808074b84150a147b1731..28763da0876b14d8698f5e4478ac2f98508d7258 100644 (file)
@@ -4,12 +4,13 @@ class VirtualMachinesTest < ActionDispatch::IntegrationTest
   test "make and name a new virtual machine" do
     Capybara.current_driver = Capybara.javascript_driver
     visit page_with_token('admin_trustedclient')
+    find('#system-menu').click
     click_link 'Virtual machines'
     assert page.has_text? 'testvm.shell'
     click_on 'Add a new virtual machine'
     find('tr', text: 'hostname').
-      find('span', text: 'none').click
-    assert page.has_text? 'Update hostname'
+      find('a[data-original-title=edit]').click
+    assert page.has_text? 'Edit hostname'
     fill_in 'editable-text', with: 'testname'
     click_button 'editable-submit'
     assert page.has_text? 'testname'
index a5681d24f4d9970b8af1a54af291f6457c4e9148..fa4f9c5697affc09c685f4ecca2ecd89c1509c2a 100644 (file)
@@ -10,7 +10,7 @@ class ResourceListTest < ActiveSupport::TestCase
 
   test 'links_for on non-empty resource list' do
     use_token :active
-    results = Group.find(api_fixture('groups')['afolder']['uuid']).contents(include_linked: true)
+    results = Group.find(api_fixture('groups')['aproject']['uuid']).contents(include_linked: true)
     assert_equal [], results.links_for(api_fixture('users')['active']['uuid'])
     assert_equal [], results.links_for(api_fixture('jobs')['running_cancelled']['uuid'])
     assert_equal [], results.links_for(api_fixture('jobs')['running']['uuid'], 'bogus-link-class')
@@ -18,25 +18,25 @@ class ResourceListTest < ActiveSupport::TestCase
   end
 
   test 'links_for returns all link classes (simulated results)' do
-    folder_uuid = api_fixture('groups')['afolder']['uuid']
-    specimen_uuid = api_fixture('specimens')['in_afolder']['uuid']
+    project_uuid = api_fixture('groups')['aproject']['uuid']
+    specimen_uuid = api_fixture('specimens')['in_aproject']['uuid']
     api_response = {
       kind: 'arvados#specimenList',
       links: [{kind: 'arvados#link',
                 uuid: 'zzzzz-o0j2j-asdfasdfasdfas0',
-                tail_uuid: folder_uuid,
+                tail_uuid: project_uuid,
                 head_uuid: specimen_uuid,
                 link_class: 'name',
                 name: 'Alice'},
               {kind: 'arvados#link',
                 uuid: 'zzzzz-o0j2j-asdfasdfasdfas1',
-                tail_uuid: folder_uuid,
+                tail_uuid: project_uuid,
                 head_uuid: specimen_uuid,
                 link_class: 'foo',
                 name: 'Bob'},
               {kind: 'arvados#link',
                 uuid: 'zzzzz-o0j2j-asdfasdfasdfas2',
-                tail_uuid: folder_uuid,
+                tail_uuid: project_uuid,
                 head_uuid: specimen_uuid,
                 link_class: nil,
                 name: 'Clydesdale'}],
index 4a7144f99ae098feeaed448d3d099222e2662048..435463a83e0bbd66682339649d9ffd1b7c70ef9f 100644 (file)
@@ -4,7 +4,7 @@ class GroupTest < ActiveSupport::TestCase
   test "get contents with names" do
     use_token :active
     oi = Group.
-      find(api_fixture('groups')['asubfolder']['uuid']).
+      find(api_fixture('groups')['asubproject']['uuid']).
       contents(include_linked: true)
     assert_operator(0, :<, oi.count,
                     "Expected to find some items belonging to :active user")
@@ -14,14 +14,14 @@ class GroupTest < ActiveSupport::TestCase
                     "Expected to receive name links with contents response")
     oi_uuids = oi.collect { |i| i['uuid'] }
 
-    expect_uuid = api_fixture('specimens')['in_asubfolder']['uuid']
+    expect_uuid = api_fixture('specimens')['in_asubproject']['uuid']
     assert_includes(oi_uuids, expect_uuid,
-                    "Expected '#{expect_uuid}' in asubfolder's contents")
+                    "Expected '#{expect_uuid}' in asubproject's contents")
 
-    expect_uuid = api_fixture('specimens')['in_afolder_linked_from_asubfolder']['uuid']
-    expect_name = api_fixture('links')['specimen_is_in_two_folders']['name']
+    expect_uuid = api_fixture('specimens')['in_aproject_linked_from_asubproject']['uuid']
+    expect_name = api_fixture('links')['specimen_is_in_two_projects']['name']
     assert_includes(oi_uuids, expect_uuid,
-                    "Expected '#{expect_uuid}' in asubfolder's contents")
+                    "Expected '#{expect_uuid}' in asubproject's contents")
     assert_equal(expect_name, oi.name_for(expect_uuid),
                  "Expected name_for '#{expect_uuid}' to be '#{expect_name}'")
   end
diff --git a/apps/workbench/test/unit/helpers/folders_helper_test.rb b/apps/workbench/test/unit/helpers/folders_helper_test.rb
deleted file mode 100644 (file)
index 5b4c557..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class FoldersHelperTest < ActionView::TestCase
-end
diff --git a/apps/workbench/test/unit/helpers/projects_helper_test.rb b/apps/workbench/test/unit/helpers/projects_helper_test.rb
new file mode 100644 (file)
index 0000000..a591e4e
--- /dev/null
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class ProjectsHelperTest < ActionView::TestCase
+end
diff --git a/services/api/db/migrate/20140607150616_rename_folder_to_project.rb_deferred b/services/api/db/migrate/20140607150616_rename_folder_to_project.rb_deferred
new file mode 100644 (file)
index 0000000..2a3c1d3
--- /dev/null
@@ -0,0 +1,9 @@
+class RenameFolderToProject < ActiveRecord::Migration
+  def up
+    Group.update_all("group_class = 'project'", "group_class = 'folder'")
+  end
+
+  def down
+    Group.update_all("group_class = 'folder'", "group_class = 'project'")
+  end
+end
index 96db93c35e1e1c812910654b78f98665ef833499..e41ca8a5c5cf4ed9edc4ee407acde14dd7f693f6 100644 (file)
@@ -44,7 +44,7 @@ testusergroup_admins:
   owner_uuid: zzzzz-tpzed-000000000000000
   name: Administrators of a subset of users
 
-afolder:
+aproject:
   uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-04-21 15:37:48 -0400
@@ -52,11 +52,11 @@ afolder:
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   modified_at: 2014-04-21 15:37:48 -0400
   updated_at: 2014-04-21 15:37:48 -0400
-  name: A Folder
-  description: Test folder belonging to active user
-  group_class: folder
+  name: A Project
+  description: Test project belonging to active user
+  group_class: project
 
-asubfolder:
+asubproject:
   uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
@@ -64,8 +64,8 @@ asubfolder:
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   modified_at: 2014-04-21 15:37:48 -0400
   updated_at: 2014-04-21 15:37:48 -0400
-  name: A Subfolder
-  description: "Test folder belonging to active user's first test folder"
+  name: A Subproject
+  description: "Test project belonging to active user's first test project"
   group_class: folder
 
 bad_group_has_ownership_cycle_a:
index 1fb1608f4de7b69ce881cb10c2a187edb90c4008..397659e46d0dc59b6c56b0f3a5eef3014cf45ff1 100644 (file)
@@ -304,7 +304,7 @@ test_timestamps:
   name: test
   properties: {}
 
-specimen_is_in_two_folders:
+specimen_is_in_two_projects:
   uuid: zzzzz-o0j2j-ryhm1bn83ni03sn
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   created_at: 2014-04-21 15:37:48 -0400
@@ -315,10 +315,10 @@ specimen_is_in_two_folders:
   tail_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   head_uuid: zzzzz-j58dm-5gid26432uujf79
   link_class: name
-  name: "I'm in a subfolder, too"
+  name: "I'm in a subproject, too"
   properties: {}
 
-template_name_in_afolder:
+template_name_in_aproject:
   uuid: zzzzz-o0j2j-4kpwf3d6rwkeqhl
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-04-29 16:47:26 -0400
@@ -329,10 +329,10 @@ template_name_in_afolder:
   tail_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   head_uuid: zzzzz-p5p6p-aox0k0ofxrystgw
   link_class: name
-  name: "I'm a template in a folder"
+  name: "I'm a template in a project"
   properties: {}
 
-job_name_in_afolder:
+job_name_in_aproject:
   uuid: zzzzz-o0j2j-1kt6dppqcxbl1yt
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-04-29 16:47:26 -0400
@@ -343,11 +343,11 @@ job_name_in_afolder:
   tail_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   head_uuid: zzzzz-8i9sb-pshmckwoma9plh7
   link_class: name
-  name: "I'm a job in a folder"
+  name: "I'm a job in a project"
   properties: {}
 
-foo_collection_name_in_afolder:
-  uuid: zzzzz-o0j2j-foofoldername12
+foo_collection_name_in_aproject:
+  uuid: zzzzz-o0j2j-fooprojectname1
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-04-21 15:37:48 -0400
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -358,7 +358,7 @@ foo_collection_name_in_afolder:
   head_uuid: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   link_class: name
   # This should resemble the default name assigned when a
-  # Collection is added to a Folder.
+  # Collection is added to a Project.
   name: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45 added sometime"
   properties: {}
 
index b5e1bc1c6bf2e44683b2ff18eccfc7593c8c8818..823116fda412b805b9bc76dbc40065b7eb99c889 100644 (file)
@@ -21,3 +21,19 @@ has_component_with_empty_script_parameters:
    foo:
     script: foo
     script_version: master
+
+has_job:
+  name: pipeline_with_job
+  state: Ready
+  uuid: zzzzz-d1hrv-1yfj6xkidf2muk3
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  components:
+   foo:
+    script: foo
+    script_version: master
+    script_parameters: {}
+    job: {
+            uuid: zzzzz-8i9sb-pshmckwoma9plh7,
+            script_version: master
+         }
index c48bff71470606c31f8aaf4f0f10bd60c6d1f71f..4528070a528ee86c93cf3752ce0e86d15229ae6d 100644 (file)
@@ -16,25 +16,25 @@ owned_by_spectator:
   created_at: 2014-04-21 15:37:48 -0400
   modified_at: 2014-04-21 15:37:48 -0400
 
-in_afolder:
+in_aproject:
   uuid: zzzzz-j58dm-7r18rnd5nzhg5yk
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
   modified_at: 2014-04-21 15:37:48 -0400
 
-in_asubfolder:
+in_asubproject:
   uuid: zzzzz-j58dm-c40lddwcqqr1ffs
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   created_at: 2014-04-21 15:37:48 -0400
   modified_at: 2014-04-21 15:37:48 -0400
 
-in_afolder_linked_from_asubfolder:
+in_aproject_linked_from_asubproject:
   uuid: zzzzz-j58dm-5gid26432uujf79
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
   modified_at: 2014-04-21 15:37:48 -0400
 
-owned_by_afolder_with_no_name_link:
+owned_by_aproject_with_no_name_link:
   uuid: zzzzz-j58dm-ypsjlol9dofwijz
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-05-05 04:11:52 -0400
index a5582e66b5f98c474c0ebbdac69a45a4462305e8..2e8e231a9723a78b10af6dbb747044d526e76307 100644 (file)
@@ -5,12 +5,12 @@ class Arvados::V1::FiltersTest < ActionController::TestCase
     @controller = Arvados::V1::GroupsController.new
     authorize_with :admin
     get :index, {
-      filters: [ ['group_class', 'not in', ['folder']] ],
+      filters: [ ['group_class', 'not in', ['project']] ],
       controller: 'groups',
     }
     assert_response :success
     found = assigns(:objects)
     assert_includes(found.collect(&:group_class), nil,
-                    "'group_class not in ['folder']' filter should pass null")
+                    "'group_class not in ['project']' filter should pass null")
   end
 end
index f8f9eaeb0d5224d2cbb95f6179fbe835dde0a0ac..49e9b7d6c6471be8be1f961ef1707bcc73bd9d26 100644 (file)
@@ -14,22 +14,22 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
     assert_response 403
   end
 
-  test "get list of folders" do
+  test "get list of projects" do
     authorize_with :active
-    get :index, filters: [['group_class', '=', 'folder']], format: :json
+    get :index, filters: [['group_class', 'in', ['project', 'folder']]], format: :json
     assert_response :success
     group_uuids = []
     json_response['items'].each do |group|
-      assert_equal 'folder', group['group_class']
+      assert_includes ['folder', 'project'], group['group_class']
       group_uuids << group['uuid']
     end
-    assert_includes group_uuids, groups(:afolder).uuid
-    assert_includes group_uuids, groups(:asubfolder).uuid
+    assert_includes group_uuids, groups(:aproject).uuid
+    assert_includes group_uuids, groups(:asubproject).uuid
     assert_not_includes group_uuids, groups(:system_group).uuid
     assert_not_includes group_uuids, groups(:private).uuid
   end
 
-  test "get list of groups that are not folders" do
+  test "get list of groups that are not projects" do
     authorize_with :active
     get :index, filters: [['group_class', '=', nil]], format: :json
     assert_response :success
@@ -38,8 +38,8 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
       assert_equal nil, group['group_class']
       group_uuids << group['uuid']
     end
-    assert_not_includes group_uuids, groups(:afolder).uuid
-    assert_not_includes group_uuids, groups(:asubfolder).uuid
+    assert_not_includes group_uuids, groups(:aproject).uuid
+    assert_not_includes group_uuids, groups(:asubproject).uuid
     assert_includes group_uuids, groups(:private).uuid
   end
 
@@ -57,7 +57,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   test 'get group-owned objects' do
     authorize_with :active
     get :contents, {
-      id: groups(:afolder).uuid,
+      id: groups(:aproject).uuid,
       format: :json,
       include_linked: true,
     }
@@ -72,7 +72,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   test 'get group-owned objects with limit' do
     authorize_with :active
     get :contents, {
-      id: groups(:afolder).uuid,
+      id: groups(:aproject).uuid,
       limit: 1,
       format: :json,
     }
@@ -84,7 +84,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   test 'get group-owned objects with limit and offset' do
     authorize_with :active
     get :contents, {
-      id: groups(:afolder).uuid,
+      id: groups(:aproject).uuid,
       limit: 1,
       offset: 12345,
       format: :json,
@@ -97,7 +97,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   test 'get group-owned objects with additional filter matching nothing' do
     authorize_with :active
     get :contents, {
-      id: groups(:afolder).uuid,
+      id: groups(:aproject).uuid,
       filters: [['uuid', 'in', ['foo_not_a_uuid','bar_not_a_uuid']]],
       format: :json,
     }
@@ -107,10 +107,10 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   end
 
   test 'get group-owned objects without include_linked' do
-    unexpected_uuid = specimens(:in_afolder_linked_from_asubfolder).uuid
+    unexpected_uuid = specimens(:in_aproject_linked_from_asubproject).uuid
     authorize_with :active
     get :contents, {
-      id: groups(:asubfolder).uuid,
+      id: groups(:asubproject).uuid,
       format: :json,
     }
     assert_response :success
@@ -119,10 +119,10 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   end
 
   test 'get group-owned objects with include_linked' do
-    expected_uuid = specimens(:in_afolder_linked_from_asubfolder).uuid
+    expected_uuid = specimens(:in_aproject_linked_from_asubproject).uuid
     authorize_with :active
     get :contents, {
-      id: groups(:asubfolder).uuid,
+      id: groups(:asubproject).uuid,
       include_linked: true,
       format: :json,
     }
@@ -130,7 +130,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
     uuids = json_response['items'].collect { |i| i['uuid'] }
     assert_includes uuids, expected_uuid, "Did not get #{expected_uuid}"
 
-    expected_name = links(:specimen_is_in_two_folders).name
+    expected_name = links(:specimen_is_in_two_projects).name
     found_specimen_name = false
     assert(json_response['links'].any?,
            "Expected a non-empty array of links in response")
@@ -158,7 +158,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
         # times within a test.
         @json_response = nil
         get :contents, {
-          id: groups(:afolder).uuid,
+          id: groups(:aproject).uuid,
           include_linked: inc_ind,
           limit: limit,
           offset: offset,
@@ -180,7 +180,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
           owner_received[item['owner_uuid']] = true
           offset += 1
           if not inc_ind
-            assert_equal groups(:afolder).uuid, item['owner_uuid']
+            assert_equal groups(:aproject).uuid, item['owner_uuid']
           end
         end
         break if offset >= items_available
@@ -197,7 +197,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
       test "Raise error on bogus #{arg} parameter #{val.inspect}" do
         authorize_with :active
         get :contents, {
-          :id => groups(:afolder).uuid,
+          :id => groups(:aproject).uuid,
           :format => :json,
           arg => val,
         }
@@ -209,7 +209,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   test 'get writable_by list for owned group' do
     authorize_with :active
     get :show, {
-      id: groups(:afolder).uuid,
+      id: groups(:aproject).uuid,
       format: :json
     }
     assert_response :success
index dfce78b13f7f79c4f5927bbbda749882758071c6..d5b42665c38b8599c5fa0e3bc088d851469f4f33 100644 (file)
@@ -271,11 +271,11 @@ class Arvados::V1::LinksControllerTest < ActionController::TestCase
   end
 
   test "refuse duplicate name" do
-    the_name = links(:job_name_in_afolder).name
-    the_folder = links(:job_name_in_afolder).tail_uuid
+    the_name = links(:job_name_in_aproject).name
+    the_project = links(:job_name_in_aproject).tail_uuid
     authorize_with :active
     post :create, link: {
-      tail_uuid: the_folder,
+      tail_uuid: the_project,
       head_uuid: specimens(:owned_by_active_user).uuid,
       link_class: 'name',
       name: the_name,
index 597af62ec83aa82baf96548837e74bd3c8372603..97977a5d56be7ba87bd8c2c93938b41ec36902b2 100644 (file)
@@ -18,7 +18,7 @@ class GroupTest < ActiveSupport::TestCase
     assert_equal false, s.save, "should not save object with #{g.uuid} as owner"
 
     # Use the group as the new owner of an existing object
-    s = specimens(:in_afolder)
+    s = specimens(:in_aproject)
     s.owner_uuid = groups(:bad_group_has_ownership_cycle_b).uuid
     assert s.valid?, "ownership should pass validation"
     assert_equal false, s.save, "should not save object with #{g.uuid} as owner"
index 10f2b5eca54adffb9f6250198386c4292f79037d..56a38045e4cf75fe2b8289e9f119c2e668c9b97e 100644 (file)
@@ -8,13 +8,13 @@ class LinkTest < ActiveSupport::TestCase
   end
 
   test 'name links with the same tail_uuid must be unique' do
-    a = Link.create!(tail_uuid: groups(:afolder).uuid,
+    a = Link.create!(tail_uuid: groups(:aproject).uuid,
                      head_uuid: specimens(:owned_by_active_user).uuid,
                      link_class: 'name',
                      name: 'foo')
     assert a.valid?, a.errors.to_s
     assert_raises ActiveRecord::RecordNotUnique do
-      b = Link.create!(tail_uuid: groups(:afolder).uuid,
+      b = Link.create!(tail_uuid: groups(:aproject).uuid,
                        head_uuid: specimens(:owned_by_active_user).uuid,
                        link_class: 'name',
                        name: 'foo')
@@ -22,12 +22,12 @@ class LinkTest < ActiveSupport::TestCase
   end
 
   test 'name links with different tail_uuid need not be unique' do
-    a = Link.create!(tail_uuid: groups(:afolder).uuid,
+    a = Link.create!(tail_uuid: groups(:aproject).uuid,
                      head_uuid: specimens(:owned_by_active_user).uuid,
                      link_class: 'name',
                      name: 'foo')
     assert a.valid?, a.errors.to_s
-    b = Link.create!(tail_uuid: groups(:asubfolder).uuid,
+    b = Link.create!(tail_uuid: groups(:asubproject).uuid,
                      head_uuid: specimens(:owned_by_active_user).uuid,
                      link_class: 'name',
                      name: 'foo')
@@ -38,7 +38,7 @@ class LinkTest < ActiveSupport::TestCase
 
   [nil, '', false].each do |name|
     test "name links cannot have name=#{name.inspect}" do
-      a = Link.create(tail_uuid: groups(:afolder).uuid,
+      a = Link.create(tail_uuid: groups(:aproject).uuid,
                       head_uuid: specimens(:owned_by_active_user).uuid,
                       link_class: 'name',
                       name: name)
index f159294f6b64c3b3f705f31ea3e91729a593be74..c177bc3901cdc74db85d448b32222eb586bedcb1 100644 (file)
@@ -73,7 +73,7 @@ class OwnerTest < ActiveSupport::TestCase
     end
   end
 
-  ['users(:active)', 'groups(:afolder)'].each do |ofixt|
+  ['users(:active)', 'groups(:aproject)'].each do |ofixt|
     test "delete #{ofixt} that owns other objects" do
       o = eval ofixt
       assert_equal(true, Specimen.where(owner_uuid: o.uuid).any?,