From: Ward Vandewege Date: Thu, 29 May 2014 00:29:12 +0000 (-0400) Subject: Merge branch 'master' into 2681-new-inactive-user-notification X-Git-Tag: 1.1.0~2575^2~17^2~8 X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/ebb166d65eb37f89edeccfc5be97014f3f2a73e2?hp=1eee45ce0bbae9e7d04a9382469d2c48fb0cfd3e Merge branch 'master' into 2681-new-inactive-user-notification --- diff --git a/.gitignore b/.gitignore index 2156fdf7e7..602e1b9e40 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ sdk/perl/pm_to_blib services/keep/bin services/keep/pkg services/keep/src/github.com +sdk/java/target +*.class diff --git a/apps/admin/setup-new-user.rb b/apps/admin/setup-new-user.rb deleted file mode 100755 index ab5aa2edf3..0000000000 --- a/apps/admin/setup-new-user.rb +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env ruby - -abort 'Error: Ruby >= 1.9.3 required.' if RUBY_VERSION < '1.9.3' - -require 'logger' -require 'trollop' -log = Logger.new STDERR -log.progname = $0.split('/').last - -opts = Trollop::options do - banner '' - banner "Usage: #{log.progname} " + - "{user_uuid_or_email} {user_and_repo_name} {vm_uuid}" - banner '' - opt :debug, <<-eos -Show debug messages. - eos - opt :create, <<-eos -Create a new user with the given email address if an existing user \ -is not found. - eos - opt :openid_prefix, <<-eos, default: 'https://www.google.com/accounts/o8/id' -If creating a new user record, require authentication from an OpenID \ -with this OpenID prefix *and* a matching email address in order to \ -claim the account. - eos - opt :force, <<-eos -Continue even if sanity checks raise flags: the given user is already \ -active, the given repository already exists, etc. - eos - opt :n, 'Do not change anything, just probe' -end - -log.level = (ENV['DEBUG'] || opts.debug) ? Logger::DEBUG : Logger::WARN - -if ARGV.count != 3 - Trollop::die "required arguments are missing" -end -user_arg, user_repo_name, vm_uuid = ARGV - -require 'arvados' -arv = Arvados.new(api_version: 'v1') - -# Look up the given user by uuid or, failing that, email address. -user = begin - arv.user.get(uuid: user_arg) - rescue Arvados::TransactionFailedError - found = arv.user.list(where: {email: ARGV[0]})[:items] - if found.count == 0 and opts.create - if !opts.force and !user_arg.match(/\w\@\w+\.\w+/) - abort "About to create new user, but #{user_arg.inspect} " + - "does not look like an email address. Stop." - end - if opts.n - log.info "-n flag given. Stop before creating new user record." - exit 0 - end - new_user = arv.user.create(user: {email: user_arg}) - log.info { "created user: " + new_user[:uuid] } - login_perm_props = {identity_url_prefix: opts.openid_prefix } - oid_login_perm = arv.link.create(link: { - link_class: 'permission', - name: 'can_login', - tail_kind: 'email', - tail_uuid: user_arg, - head_kind: 'arvados#user', - head_uuid: new_user[:uuid], - properties: login_perm_props - }) - log.info { "openid login permission: " + oid_login_perm[:uuid] } - found = [new_user] - end - if found.count != 1 - abort "Found #{found.count} users " + - "with uuid or email #{user_arg.inspect}. Stop." - end - found.first - end -log.info { "user uuid: " + user[:uuid] } - -# Look up the given virtual machine just to make sure it really exists. -begin - vm = arv.virtual_machine.get(uuid: vm_uuid) -rescue - abort "Could not look up virtual machine with uuid #{vm_uuid.inspect}. Stop." -end -log.info { "vm uuid: " + vm[:uuid] } - -# Look up the "All users" group (we expect uuid *-*-fffffffffffffff). -group = arv.group.list(where: {name: 'All users'})[:items].select do |g| - g[:uuid].match /-f+$/ -end.first -if not group - abort "Could not look up the 'All users' group with uuid '*-*-fffffffffffffff'. Stop." -end -log.info { "\"All users\" group uuid: " + group[:uuid] } - -# Look for signs the user has already been activated / set up. - -if user[:is_active] - log.warn "User's is_active flag is already set." - need_force = true -end - -# Look for existing repository access (perhaps using a different -# repository/user name). -repo_perms = arv.link.list(where: { - tail_uuid: user[:uuid], - head_kind: 'arvados#repository', - link_class: 'permission', - name: 'can_write'})[:items] -if [] != repo_perms - log.warn "User already has repository access " + - repo_perms.collect { |p| p[:uuid] }.inspect + "." - need_force = true -end - -# Check for an existing repository with the same name we're about to -# use. -repo = arv.repository.list(where: {name: user_repo_name})[:items].first -if repo - log.warn "Repository already exists with name #{user_repo_name.inspect}: " + - "#{repo[:uuid]}" - need_force = true -end - -if opts.n - log.info "-n flag given. Done." - exit 0 -end - -if need_force and not opts.force - abort "This does not seem to be a new user[name], and -f was not given. Stop." -end - -# Everything seems to be in order. Create a repository (if needed) and -# add permissions. - -repo ||= arv.repository.create(repository: {name: user_repo_name}) -log.info { "repo uuid: " + repo[:uuid] } - -repo_perm = arv.link.create(link: { - tail_kind: 'arvados#user', - tail_uuid: user[:uuid], - head_kind: 'arvados#repository', - head_uuid: repo[:uuid], - link_class: 'permission', - name: 'can_write'}) -log.info { "repo permission: " + repo_perm[:uuid] } - -login_perm = arv.link.create(link: { - tail_kind: 'arvados#user', - tail_uuid: user[:uuid], - head_kind: 'arvados#virtualMachine', - head_uuid: vm[:uuid], - link_class: 'permission', - name: 'can_login', - properties: {username: user_repo_name}}) -log.info { "login permission: " + login_perm[:uuid] } - -group_perm = arv.link.create(link: { - tail_kind: 'arvados#user', - tail_uuid: user[:uuid], - head_kind: 'arvados#group', - head_uuid: group[:uuid], - link_class: 'permission', - name: 'can_read'}) -log.info { "group permission: " + group_perm[:uuid] } diff --git a/apps/workbench/.gitignore b/apps/workbench/.gitignore index afb317b169..24a7a84a31 100644 --- a/apps/workbench/.gitignore +++ b/apps/workbench/.gitignore @@ -28,3 +28,10 @@ # This can be a symlink to ../../../doc/.site in dev setups /public/doc + +# SimpleCov reports +/coverage + +# Dev/test SSL certificates +/self-signed.key +/self-signed.pem diff --git a/apps/workbench/Gemfile b/apps/workbench/Gemfile index ee43a895c7..754d5c6043 100644 --- a/apps/workbench/Gemfile +++ b/apps/workbench/Gemfile @@ -1,6 +1,7 @@ source 'https://rubygems.org' -gem 'rails', '~> 3.2.0' +gem 'rails', '~> 4.1.0' +gem 'minitest', '>= 5.0.0' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' @@ -11,11 +12,17 @@ gem 'multi_json' gem 'oj' gem 'sass' +# Note: keeping this out of the "group :assets" section "may" allow us +# to use Coffescript for UJS responses. It also prevents a +# warning/problem when running tests: "WARN: tilt autoloading +# 'coffee_script' in a non thread-safe way; explicit require +# 'coffee_script' suggested." +gem 'coffee-rails' + # Gems used only for assets and not required # in production environments by default. group :assets do - gem 'sass-rails', '~> 3.2.0' - gem 'coffee-rails', '~> 3.2.0' + gem 'sass-rails' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', :platforms => :ruby @@ -29,6 +36,11 @@ group :test do gem 'capybara' gem 'poltergeist' gem 'headless' + # Note: "require: false" here tells bunder not to automatically + # 'require' the packages during application startup. Installation is + # still mandatory. + gem 'simplecov', '~> 0.7.1', require: false + gem 'simplecov-rcov', require: false end gem 'jquery-rails' @@ -59,5 +71,8 @@ gem 'RedCloth' gem 'piwik_analytics' gem 'httpclient' -gem 'themes_for_rails' + +# This fork has Rails 4 compatible routes +gem 'themes_for_rails', git: 'https://github.com/holtkampw/themes_for_rails', ref: '1fd2d7897d75ae0d6375f4c390df87b8e91ad417' + gem "deep_merge", :require => 'deep_merge/rails_compat' diff --git a/apps/workbench/Gemfile.lock b/apps/workbench/Gemfile.lock index e1e2b81954..173be13cc3 100644 --- a/apps/workbench/Gemfile.lock +++ b/apps/workbench/Gemfile.lock @@ -1,41 +1,48 @@ +GIT + remote: https://github.com/holtkampw/themes_for_rails + revision: 1fd2d7897d75ae0d6375f4c390df87b8e91ad417 + ref: 1fd2d7897d75ae0d6375f4c390df87b8e91ad417 + specs: + themes_for_rails (0.5.1) + rails (>= 3.0.0) + GEM remote: https://rubygems.org/ specs: RedCloth (4.2.9) - actionmailer (3.2.15) - actionpack (= 3.2.15) + actionmailer (4.1.1) + actionpack (= 4.1.1) + actionview (= 4.1.1) mail (~> 2.5.4) - actionpack (3.2.15) - activemodel (= 3.2.15) - activesupport (= 3.2.15) - builder (~> 3.0.0) + actionpack (4.1.1) + actionview (= 4.1.1) + activesupport (= 4.1.1) + rack (~> 1.5.2) + rack-test (~> 0.6.2) + actionview (4.1.1) + activesupport (= 4.1.1) + builder (~> 3.1) erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.5) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.2.1) - activemodel (3.2.15) - activesupport (= 3.2.15) - builder (~> 3.0.0) - activerecord (3.2.15) - activemodel (= 3.2.15) - activesupport (= 3.2.15) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activeresource (3.2.15) - activemodel (= 3.2.15) - activesupport (= 3.2.15) - activesupport (3.2.15) - i18n (~> 0.6, >= 0.6.4) - multi_json (~> 1.0) + activemodel (4.1.1) + activesupport (= 4.1.1) + builder (~> 3.1) + activerecord (4.1.1) + activemodel (= 4.1.1) + activesupport (= 4.1.1) + arel (~> 5.0.0) + activesupport (4.1.1) + i18n (~> 0.6, >= 0.6.9) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.1) + tzinfo (~> 1.1) andand (1.3.3) - arel (3.0.2) + arel (5.0.1.20140414130214) bootstrap-sass (3.1.0.1) sass (~> 3.2) bootstrap-x-editable-rails (1.5.1.1) railties (>= 3.0) - builder (3.0.4) + builder (3.2.2) capistrano (2.15.5) highline net-scp (>= 1.0.0) @@ -51,13 +58,13 @@ GEM childprocess (0.5.1) ffi (~> 1.0, >= 1.0.11) cliver (0.3.2) - coffee-rails (3.2.2) + coffee-rails (4.0.1) coffee-script (>= 2.2.0) - railties (~> 3.2.0) + railties (>= 4.0.0, < 5.0) coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.6.3) + coffee-script-source (1.7.0) commonjs (0.2.7) daemon_controller (1.1.7) deep_merge (1.0.1) @@ -68,8 +75,7 @@ GEM highline (1.6.20) hike (1.2.3) httpclient (2.3.4.1) - i18n (0.6.5) - journey (1.0.4) + i18n (0.6.9) jquery-rails (3.0.4) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) @@ -83,9 +89,10 @@ GEM mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - mime-types (1.25) + mime-types (1.25.1) mini_portile (0.5.2) - multi_json (1.8.2) + minitest (5.3.3) + multi_json (1.10.0) net-scp (1.1.2) net-ssh (>= 2.6.5) net-sftp (2.1.2) @@ -109,63 +116,68 @@ GEM cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - polyglot (0.3.3) - rack (1.4.5) - rack-cache (1.2) - rack (>= 0.4) - rack-ssl (1.3.3) - rack + polyglot (0.3.4) + rack (1.5.2) rack-test (0.6.2) rack (>= 1.0) - rails (3.2.15) - actionmailer (= 3.2.15) - actionpack (= 3.2.15) - activerecord (= 3.2.15) - activeresource (= 3.2.15) - activesupport (= 3.2.15) - bundler (~> 1.0) - railties (= 3.2.15) - railties (3.2.15) - actionpack (= 3.2.15) - activesupport (= 3.2.15) - rack-ssl (~> 1.3.2) + rails (4.1.1) + actionmailer (= 4.1.1) + actionpack (= 4.1.1) + actionview (= 4.1.1) + activemodel (= 4.1.1) + activerecord (= 4.1.1) + activesupport (= 4.1.1) + bundler (>= 1.3.0, < 2.0) + railties (= 4.1.1) + sprockets-rails (~> 2.0) + railties (4.1.1) + actionpack (= 4.1.1) + activesupport (= 4.1.1) rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (10.1.0) - rdoc (3.12.2) - json (~> 1.4) + thor (>= 0.18.1, < 2.0) + rake (10.3.1) ref (1.0.5) rubyzip (1.1.0) rvm-capistrano (1.5.1) capistrano (~> 2.15.4) sass (3.2.12) - sass-rails (3.2.6) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) + sass-rails (4.0.3) + railties (>= 4.0.0, < 5.0) + sass (~> 3.2.0) + sprockets (~> 2.8, <= 2.11.0) + sprockets-rails (~> 2.0) selenium-webdriver (2.40.0) childprocess (>= 0.5.0) multi_json (~> 1.0) rubyzip (~> 1.0) websocket (~> 1.0.4) - sprockets (2.2.2) + simplecov (0.7.1) + multi_json (~> 1.0) + simplecov-html (~> 0.7.1) + simplecov-html (0.7.1) + simplecov-rcov (0.2.3) + simplecov (>= 0.4.1) + sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) + sprockets-rails (2.1.3) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (~> 2.8) sqlite3 (1.3.8) - themes_for_rails (0.5.1) - rails (>= 3.0.0) therubyracer (0.12.0) libv8 (~> 3.16.14.0) ref - thor (0.18.1) + thor (0.19.1) + thread_safe (0.3.3) tilt (1.4.1) treetop (1.4.15) polyglot polyglot (>= 0.3.1) - tzinfo (0.3.38) + tzinfo (1.1.0) + thread_safe (~> 0.1) uglifier (2.3.1) execjs (>= 0.3.0) json (>= 1.8.0) @@ -183,24 +195,27 @@ DEPENDENCIES bootstrap-sass (~> 3.1.0) bootstrap-x-editable-rails capybara - coffee-rails (~> 3.2.0) + coffee-rails deep_merge headless httpclient jquery-rails less less-rails + minitest (>= 5.0.0) multi_json oj passenger piwik_analytics poltergeist - rails (~> 3.2.0) + rails (~> 4.1.0) rvm-capistrano sass - sass-rails (~> 3.2.0) + sass-rails selenium-webdriver + simplecov (~> 0.7.1) + simplecov-rcov sqlite3 - themes_for_rails + themes_for_rails! therubyracer uglifier (>= 1.0.3) diff --git a/apps/workbench/app/assets/javascripts/api_client_authorizations.js.coffee b/apps/workbench/app/assets/javascripts/api_client_authorizations.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/api_client_authorizations.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/application.js b/apps/workbench/app/assets/javascripts/application.js index 189063bcff..d66cb9224f 100644 --- a/apps/workbench/app/assets/javascripts/application.js +++ b/apps/workbench/app/assets/javascripts/application.js @@ -29,7 +29,6 @@ jQuery(function($){ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') } }); - $('.editable').editable(); $('[data-toggle=tooltip]').tooltip(); $('.expand-collapse-row').on('click', function(event) { diff --git a/apps/workbench/app/assets/javascripts/authorized_keys.js.coffee b/apps/workbench/app/assets/javascripts/authorized_keys.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/authorized_keys.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/collections.js b/apps/workbench/app/assets/javascripts/collections.js index 7f4b510316..e957835127 100644 --- a/apps/workbench/app/assets/javascripts/collections.js +++ b/apps/workbench/app/assets/javascripts/collections.js @@ -3,7 +3,6 @@ jQuery(function($){ var toggle_group = $(this).parents('[data-remote-href]').first(); var want_persist = !toggle_group.find('button').hasClass('active'); var want_state = want_persist ? 'persistent' : 'cache'; - console.log(want_persist); toggle_group.find('button'). toggleClass('active', want_persist). html(want_persist ? 'Persistent' : 'Cache'); diff --git a/apps/workbench/app/assets/javascripts/editable.js b/apps/workbench/app/assets/javascripts/editable.js index e6799bf78b..0514c1e67f 100644 --- a/apps/workbench/app/assets/javascripts/editable.js +++ b/apps/workbench/app/assets/javascripts/editable.js @@ -1,4 +1,4 @@ -$.fn.editable.defaults.ajaxOptions = {type: 'put', dataType: 'json'}; +$.fn.editable.defaults.ajaxOptions = {type: 'post', dataType: 'json'}; $.fn.editable.defaults.send = 'always'; // Default for editing is popup. I experimented with inline which is a little @@ -9,12 +9,27 @@ $.fn.editable.defaults.send = 'always'; // too narrow, when the popup box will just move to do the right thing. //$.fn.editable.defaults.mode = 'inline'; +$.fn.editable.defaults.success = function (response, newValue) { + $(document).trigger('editable:success', [this, response, newValue]); +}; + $.fn.editable.defaults.params = function (params) { var a = {}; var key = params.pk.key; - a.id = params.pk.id; - a[key] = {}; + a.id = $(this).attr('data-object-uuid') || params.pk.id; + a[key] = params.pk.defaults || {}; + // Remove null values. Otherwise they get transmitted as empty + // strings in request params. + for (i in a[key]) { + if (a[key][i] == null) + delete a[key][i]; + } a[key][params.name] = params.value; + if (!a.id) { + a['_method'] = 'post'; + } else { + a['_method'] = 'put'; + } return a; }; @@ -24,6 +39,44 @@ $.fn.editable.defaults.validate = function (value) { } } +$(document). + on('ready ajax:complete', function() { + $('#editable-submit').click(function() { + console.log($(this)); + }); + $('.editable'). + editable({ + success: function(response, newValue) { + // If we just created a new object, stash its UUID + // so we edit it next time instead of creating + // another new object. + if (!$(this).attr('data-object-uuid') && response.uuid) { + $(this).attr('data-object-uuid', response.uuid); + } + if (response.href) { + $(this).editable('option', 'url', response.href); + } + return; + } + }). + on('hidden', function(e, reason) { + // After saving a new attribute, update the same + // information if it appears elsewhere on the page. + if (reason != 'save') return; + var html = $(this).html(); + var uuid = $(this).attr('data-object-uuid'); + var attr = $(this).attr('data-name'); + var edited = this; + if (uuid && attr) { + $("[data-object-uuid='" + uuid + "']" + + "[data-name='" + attr + "']").each(function() { + if (this != edited) + $(this).html(html); + }); + } + }); + }); + $.fn.editabletypes.text.defaults.tpl = '' $.fn.editableform.buttons = '\ diff --git a/apps/workbench/app/assets/javascripts/event_log.js b/apps/workbench/app/assets/javascripts/event_log.js new file mode 100644 index 0000000000..0ebb999b76 --- /dev/null +++ b/apps/workbench/app/assets/javascripts/event_log.js @@ -0,0 +1,45 @@ +/* + * This js establishes a websockets connection with the API Server. + */ + +/* The subscribe method takes a window element id and object id. + Any log events for that particular object id are sent to that window element. */ +function subscribeToEventLog (elementId) { + // if websockets are not supported by browser, do not subscribe for events + websocketsSupported = ('WebSocket' in window); + if (websocketsSupported == false) { + return; + } + + // grab websocket connection from window, if one exists + event_log_disp = $(window).data("arv-websocket"); + if (event_log_disp == null) { + // create the event log dispatcher + websocket_url = $('meta[name=arv-websocket-url]').attr("content"); + if (websocket_url == null) + return; + + event_log_disp = new WebSocket(websocket_url); + + event_log_disp.onopen = onEventLogDispatcherOpen; + event_log_disp.onmessage = onEventLogDispatcherMessage; + + // store websocket in window to allow reuse when multiple divs subscribe for events + $(window).data("arv-websocket", event_log_disp); + } +} + +/* send subscribe message to the websockets server */ +function onEventLogDispatcherOpen(event) { + this.send('{"method":"subscribe"}'); +} + +/* trigger event for all applicable elements waiting for this event */ +function onEventLogDispatcherMessage(event) { + parsedData = JSON.parse(event.data); + object_uuid = parsedData.object_uuid; + + // if there are any listeners for this object uuid or "all", trigger the event + matches = ".arv-log-event-listener[data-object-uuid=\"" + object_uuid + "\"],.arv-log-event-listener[data-object-uuids~=\"" + object_uuid + "\"],.arv-log-event-listener[data-object-uuid=\"all\"]"; + $(matches).trigger('arv-log-event', event.data); +} diff --git a/apps/workbench/app/assets/javascripts/folders.js b/apps/workbench/app/assets/javascripts/folders.js new file mode 100644 index 0000000000..10695cf158 --- /dev/null +++ b/apps/workbench/app/assets/javascripts/folders.js @@ -0,0 +1,12 @@ +$(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/groups.js.coffee b/apps/workbench/app/assets/javascripts/groups.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/groups.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/humans.js.coffee b/apps/workbench/app/assets/javascripts/humans.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/humans.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/job_tasks.js.coffee b/apps/workbench/app/assets/javascripts/job_tasks.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/job_tasks.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/jobs.js.coffee b/apps/workbench/app/assets/javascripts/jobs.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/jobs.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/keep_disks.js.coffee b/apps/workbench/app/assets/javascripts/keep_disks.js.coffee index 761567942f..e4aa4b4321 100644 --- a/apps/workbench/app/assets/javascripts/keep_disks.js.coffee +++ b/apps/workbench/app/assets/javascripts/keep_disks.js.coffee @@ -1,3 +1,28 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ +cache_age_in_days = (milliseconds_age) -> + ONE_DAY = 1000 * 60 * 60 * 24 + milliseconds_age / ONE_DAY + +cache_age_hover = (milliseconds_age) -> + 'Cache age ' + cache_age_in_days(milliseconds_age).toFixed(1) + ' days.' + +cache_age_axis_label = (milliseconds_age) -> + cache_age_in_days(milliseconds_age).toFixed(0) + ' days' + +float_as_percentage = (proportion) -> + (proportion.toFixed(4) * 100) + '%' + +$.renderHistogram = (histogram_data) -> + Morris.Area({ + element: 'cache-age-vs-disk-histogram', + pointSize: 0, + lineWidth: 0, + data: histogram_data, + xkey: 'age', + ykeys: ['persisted', 'cache'], + labels: ['Persisted Storage Disk Utilization', 'Cached Storage Disk Utilization'], + ymax: 1, + ymin: 0, + xLabelFormat: cache_age_axis_label, + yLabelFormat: float_as_percentage, + dateFormat: cache_age_hover + }) diff --git a/apps/workbench/app/assets/javascripts/links.js.coffee b/apps/workbench/app/assets/javascripts/links.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/links.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/logs.js.coffee b/apps/workbench/app/assets/javascripts/logs.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/logs.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/nodes.js.coffee b/apps/workbench/app/assets/javascripts/nodes.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/nodes.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/pipeline_instances.js b/apps/workbench/app/assets/javascripts/pipeline_instances.js index ee14e3b781..d23ced7fbc 100644 --- a/apps/workbench/app/assets/javascripts/pipeline_instances.js +++ b/apps/workbench/app/assets/javascripts/pipeline_instances.js @@ -1,46 +1,74 @@ +function run_pipeline_button_state() { + var a = $('a.editable.required.editable-empty'); + if (a.length > 0) { + $(".run-pipeline-button").addClass("disabled"); + } + else { + $(".run-pipeline-button").removeClass("disabled"); + } +} -(function() { - var run_pipeline_button_state = function() { - var a = $('a.editable.required.editable-empty'); - if (a.length > 0) { - $("#run-pipeline-button").addClass("disabled"); +$(document).on('editable:success', function(event, tag, response, newValue) { + var $tag = $(tag); + if ($('.run-pipeline-button').length == 0) + return; + if ($tag.hasClass("required")) { + if (newValue && newValue.trim() != "") { + $tag.removeClass("editable-empty"); + $tag.parent().css("background-color", ""); + $tag.parent().prev().css("background-color", ""); } else { - $("#run-pipeline-button").removeClass("disabled"); + $tag.addClass("editable-empty"); + $tag.parent().css("background-color", "#ffdddd"); + $tag.parent().prev().css("background-color", "#ffdddd"); } } - - $.fn.editable.defaults.success = function (response, newValue) { - var tag = $(this); - if (tag.hasClass("required")) { - if (newValue && newValue.trim() != "") { - tag.removeClass("editable-empty"); - tag.parent().css("background-color", ""); - tag.parent().prev().css("background-color", ""); - } - else { - tag.addClass("editable-empty"); - tag.parent().css("background-color", "#ffdddd"); - tag.parent().prev().css("background-color", "#ffdddd"); - } - } - run_pipeline_button_state(); + if ($tag.attr('data-name')) { + // Update other inputs representing the same piece of data + $('.editable[data-name="' + $tag.attr('data-name') + '"]'). + editable('setValue', newValue); } + run_pipeline_button_state(); +}); - $(window).on('load', function() { - var a = $('a.editable.required'); - for (var i = 0; i < a.length; i++) { - var tag = $(a[i]); - if (tag.hasClass("editable-empty")) { - tag.parent().css("background-color", "#ffdddd"); - tag.parent().prev().css("background-color", "#ffdddd"); - } - else { - tag.parent().css("background-color", ""); - tag.parent().prev().css("background-color", ""); - } +$(document).on('ready ajax:complete', function() { + $('a.editable.required').each(function() { + var $tag = $(this); + if ($tag.hasClass("editable-empty")) { + $tag.parent().css("background-color", "#ffdddd"); + $tag.parent().prev().css("background-color", "#ffdddd"); + } + else { + $tag.parent().css("background-color", ""); + $tag.parent().prev().css("background-color", ""); } - run_pipeline_button_state(); - } ); + }); + run_pipeline_button_state(); +}); -})(); +$(document).on('ajax:complete ready', function() { + var a = $('.arv-log-event-listener'); + if (a.length > 0) { + $('.arv-log-event-listener').each(function() { + subscribeToEventLog(this.id); + }); + } +}); + +$(document).on('arv-log-event', '.arv-log-event-handler-append-logs', function(event, eventData){ + parsedData = JSON.parse(eventData); + + propertyText = undefined + + properties = parsedData.properties; + if (properties !== null) { + propertyText = properties.text; + } + + if (propertyText !== undefined) { + $(this).append(propertyText + "
"); + } else { + $(this).append(parsedData.summary + "
"); + } +}); diff --git a/apps/workbench/app/assets/javascripts/pipeline_templates.js.coffee b/apps/workbench/app/assets/javascripts/pipeline_templates.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/pipeline_templates.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/repositories.js.coffee b/apps/workbench/app/assets/javascripts/repositories.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/repositories.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/select_modal.js b/apps/workbench/app/assets/javascripts/select_modal.js new file mode 100644 index 0000000000..85d97c9982 --- /dev/null +++ b/apps/workbench/app/assets/javascripts/select_modal.js @@ -0,0 +1,39 @@ +$(document).on('click', '.selectable', function() { + var $this = $(this); + if (!$this.hasClass('multiple')) { + $this.closest('.selectable-container'). + find('.selectable'). + removeClass('active'); + } + $this.toggleClass('active'); +}).on('click', '.modal button[data-action-href]', function() { + var selection = []; + var data = {}; + var $modal = $(this).closest('.modal'); + $modal.find('.modal-error').removeClass('hide').hide(); + $modal.find('.selectable.active[data-object-uuid]').each(function() { + selection.push($(this).attr('data-object-uuid')); + }); + 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}}). + 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."; + } + this.modal.find('.modal-error'). + html('
' + message + '
'). + show(); + }). + success(function() { + this.modal.find('.modal-error').hide(); + window.location.reload(); + }); +}); diff --git a/apps/workbench/app/assets/javascripts/selection.js b/apps/workbench/app/assets/javascripts/selection.js index d70794dc0a..1e32c63564 100644 --- a/apps/workbench/app/assets/javascripts/selection.js +++ b/apps/workbench/app/assets/javascripts/selection.js @@ -49,15 +49,21 @@ jQuery(function($){ } var update_count = function(e) { + var html; + var this_object_uuid = $('#selection-form-content'). + closest('form'). + find('input[name=uuid]').val(); var lst = get_selection_list(); $("#persistent-selection-count").text(lst.length); if (lst.length > 0) { - $('#selection-form-content').html( - '
  • Clear selections
  • ' - + '
  • ' - + '
  • '); + html = '
  • Clear selections
  • '; + if (this_object_uuid.match('-j7d0g-')) + html += '
  • '; + html += '
  • ' + + '
  • '; + $('#selection-form-content').html(html); for (var i = 0; i < lst.length; i++) { $('#selection-form-content > li > table').append("" @@ -119,7 +125,7 @@ jQuery(function($){ }); add_form_selection_sources = null; -select_form_sources = null; +select_form_sources = null; (function() { var form_selection_sources = {}; diff --git a/apps/workbench/app/assets/javascripts/sessions.js.coffee b/apps/workbench/app/assets/javascripts/sessions.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/sessions.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/sizing.js b/apps/workbench/app/assets/javascripts/sizing.js index 55d2301387..640893fe0c 100644 --- a/apps/workbench/app/assets/javascripts/sizing.js +++ b/apps/workbench/app/assets/javascripts/sizing.js @@ -23,7 +23,7 @@ function smart_scroll_fixup(s) { a = s[i]; var h = window.innerHeight - a.getBoundingClientRect().top - 20; height = String(h) + "px"; - a.style.height = height; + a.style['max-height'] = height; } } diff --git a/apps/workbench/app/assets/javascripts/specimens.js.coffee b/apps/workbench/app/assets/javascripts/specimens.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/specimens.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/traits.js.coffee b/apps/workbench/app/assets/javascripts/traits.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/traits.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/user_agreements.js.coffee b/apps/workbench/app/assets/javascripts/user_agreements.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/user_agreements.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/users.js.coffee b/apps/workbench/app/assets/javascripts/users.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/users.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/javascripts/virtual_machines.js.coffee b/apps/workbench/app/assets/javascripts/virtual_machines.js.coffee deleted file mode 100644 index 761567942f..0000000000 --- a/apps/workbench/app/assets/javascripts/virtual_machines.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/apps/workbench/app/assets/stylesheets/application.css.scss b/apps/workbench/app/assets/stylesheets/application.css.scss index 455e4c0a9f..51c96d7fc8 100644 --- a/apps/workbench/app/assets/stylesheets/application.css.scss +++ b/apps/workbench/app/assets/stylesheets/application.css.scss @@ -40,10 +40,17 @@ table.table-justforlayout>tbody>tr>th{ table.table-justforlayout { margin-bottom: 0; } +.smaller-text { + font-size: .8em; +} .deemphasize { font-size: .8em; color: #888; } +.arvados-uuid { + font-size: .8em; + font-family: monospace; +} table .data-size, .table .data-size { text-align: right; } @@ -87,25 +94,6 @@ form.small-form-margin { text-decoration: none; text-shadow: 0 1px 0 #ffffff; } -/*.navbar .nav .dropdown .dropdown-menu li a { - padding: 2px 20px; -}*/ - -ul.arvados-nav { - list-style: none; - padding-left: 0em; - margin-left: 0em; -} - -ul.arvados-nav li ul { - list-style: none; - padding-left: 0; -} - -ul.arvados-nav li ul li { - list-style: none; - padding-left: 1em; -} .dax { max-width: 10%; @@ -147,20 +135,6 @@ span.removable-tag-container { li.notification { padding: 10px; } -.arvados-nav-container { - top: 70px; - height: calc(100% - 70px); - overflow: auto; - z-index: 2; -} - -.arvados-nav-active { - background: rgb(66, 139, 202); -} - -.arvados-nav-active a, .arvados-nav-active a:hover { - color: white; -} // See HeaderRowFixer in application.js table.table-fixed-header-row { @@ -185,3 +159,35 @@ table.table-fixed-header-row tbody { overflow-y: auto; } +.row-fill-height, .row-fill-height>div[class*='col-'] { + display: flex; +} +.row-fill-height>div[class*='col-']>div { + width: 100%; +} + +/* Show editable popover above side-nav */ +.editable-popup.popover { + z-index:1055; +} + +.navbar-nav.side-nav { + box-shadow: inset -1px 0 #e7e7e7; +} +.navbar-nav.side-nav > li:first-child { + margin-top: 5px; /* keep "hover" bg below top nav bottom border */ +} +.navbar-nav.side-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; +} +.navbar-nav.side-nav > li.dropdown > ul.dropdown-menu > li > a { + padding-top: 5px; + padding-bottom: 5px; +} +.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; +} diff --git a/apps/workbench/app/assets/stylesheets/cards.css.scss b/apps/workbench/app/assets/stylesheets/cards.css.scss new file mode 100644 index 0000000000..c9560adf86 --- /dev/null +++ b/apps/workbench/app/assets/stylesheets/cards.css.scss @@ -0,0 +1,85 @@ +.card { + padding-top: 20px; + margin: 10px 0 20px 0; + background-color: #ffffff; + border: 1px solid #d8d8d8; + border-top-width: 0; + border-bottom-width: 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.card.arvados-object { + position: relative; + display: inline-block; + width: 170px; + height: 175px; + padding-top: 0; + margin-left: 20px; + overflow: hidden; + vertical-align: top; +} +.card.arvados-object .card-top.green { + background-color: #53a93f; +} +.card.arvados-object .card-top.blue { + background-color: #427fed; +} +.card.arvados-object .card-top { + position: absolute; + top: 0; + left: 0; + display: inline-block; + width: 170px; + height: 25px; + background-color: #ffffff; +} +.card.arvados-object .card-info { + position: absolute; + top: 25px; + display: inline-block; + width: 100%; + height: 101px; + overflow: hidden; + background: #ffffff; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.card.arvados-object .card-info .title { + display: block; + margin: 8px 14px 0 14px; + overflow: hidden; + font-size: 16px; + font-weight: bold; + line-height: 18px; + color: #404040; +} +.card.arvados-object .card-info .desc { + display: block; + margin: 8px 14px 0 14px; + overflow: hidden; + font-size: 12px; + line-height: 16px; + color: #737373; + text-overflow: ellipsis; +} +.card.arvados-object .card-bottom { + position: absolute; + bottom: 0; + left: 0; + display: inline-block; + width: 100%; + padding: 10px 20px; + line-height: 29px; + text-align: center; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} diff --git a/apps/workbench/app/assets/stylesheets/collections.css.scss b/apps/workbench/app/assets/stylesheets/collections.css.scss index 24b08fa03c..2bd9bd8399 100644 --- a/apps/workbench/app/assets/stylesheets/collections.css.scss +++ b/apps/workbench/app/assets/stylesheets/collections.css.scss @@ -1,3 +1,49 @@ +/* Style for _show_files tree view. */ + +ul#collection_files { + padding: 0 .5em; +} + +ul.collection_files { + line-height: 2.5em; + list-style-type: none; + padding-left: 2.3em; +} + +ul.collection_files li { + clear: both; +} + +.collection_files_row { + padding: 1px; /* Replaced by border for :hover */ +} + +.collection_files_row:hover { + background-color: #D9EDF7; + padding: 0px; + border: 1px solid #BCE8F1; + border-radius: 3px; +} + +.collection_files_inline { + clear: both; + width: 80%; + height: auto; + max-height: 6em; + margin: 0 1em; +} + +.collection_files_name { + padding-left: .5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.collection_files_name i.fa-fw:first-child { + width: 1.6em; +} + /* "active" and "inactive" colors are too similar for a toggle switch in the default bootstrap theme. diff --git a/apps/workbench/app/assets/stylesheets/folders.css.scss b/apps/workbench/app/assets/stylesheets/folders.css.scss new file mode 100644 index 0000000000..a033e87058 --- /dev/null +++ b/apps/workbench/app/assets/stylesheets/folders.css.scss @@ -0,0 +1,13 @@ +.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; +} diff --git a/apps/workbench/app/assets/stylesheets/keep_disks.css.scss b/apps/workbench/app/assets/stylesheets/keep_disks.css.scss index 1f7780bbb0..e7a1b12c96 100644 --- a/apps/workbench/app/assets/stylesheets/keep_disks.css.scss +++ b/apps/workbench/app/assets/stylesheets/keep_disks.css.scss @@ -1,3 +1,11 @@ // Place all the styles related to the KeepDisks controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ + +/* Margin allows us some space between the table above. */ +div.graph { + margin-top: 20px; +} +div.graph h3, div.graph h4 { + text-align: center; +} diff --git a/apps/workbench/app/assets/stylesheets/sb-admin.css.scss b/apps/workbench/app/assets/stylesheets/sb-admin.css.scss new file mode 100644 index 0000000000..9bae214882 --- /dev/null +++ b/apps/workbench/app/assets/stylesheets/sb-admin.css.scss @@ -0,0 +1,164 @@ +/* +Author: Start Bootstrap - http://startbootstrap.com +'SB Admin' HTML Template by Start Bootstrap + +All Start Bootstrap themes are licensed under Apache 2.0. +For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap.com! +*/ + +/* ATTN: This is mobile first CSS - to update 786px and up screen width use the media query near the bottom of the document! */ + +/* Global Styles */ + +body { + margin-top: 50px; +} + +#wrapper { + padding-left: 0; +} + +#page-wrapper { + width: 100%; + padding: 5px 15px; +} + +/* Nav Messages */ + +.messages-dropdown .dropdown-menu .message-preview .avatar, +.messages-dropdown .dropdown-menu .message-preview .name, +.messages-dropdown .dropdown-menu .message-preview .message, +.messages-dropdown .dropdown-menu .message-preview .time { + display: block; +} + +.messages-dropdown .dropdown-menu .message-preview .avatar { + float: left; + margin-right: 15px; +} + +.messages-dropdown .dropdown-menu .message-preview .name { + font-weight: bold; +} + +.messages-dropdown .dropdown-menu .message-preview .message { + font-size: 12px; +} + +.messages-dropdown .dropdown-menu .message-preview .time { + font-size: 12px; +} + + +/* Nav Announcements */ + +.announcement-heading { + font-size: 50px; + margin: 0; +} + +.announcement-text { + margin: 0; +} + +/* Table Headers */ + +table.tablesorter thead { + cursor: pointer; +} + +table.tablesorter thead tr th:hover { + background-color: #f5f5f5; +} + +/* Flot Chart Containers */ + +.flot-chart { + display: block; + height: 400px; +} + +.flot-chart-content { + width: 100%; + height: 100%; +} + +/* Edit Below to Customize Widths > 768px */ +@media (min-width:768px) { + + /* Wrappers */ + + #wrapper { + padding-left: 225px; + } + + #page-wrapper { + padding: 15px 25px; + } + + /* Side Nav */ + + .side-nav { + margin-left: -225px; + left: 225px; + width: 225px; + position: fixed; + top: 50px; + height: calc(100% - 50px); + border-radius: 0; + border: none; + background-color: #f8f8f8; + overflow-y: auto; + overflow-x: hidden; /* no left nav scroll bar */ + } + + /* Bootstrap Default Overrides - Customized Dropdowns for the Side Nav */ + + .side-nav>li.dropdown>ul.dropdown-menu { + position: relative; + min-width: 225px; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + background-color: transparent; + box-shadow: none; + -webkit-box-shadow: none; + } + + .side-nav>li.dropdown>ul.dropdown-menu>li>a { + color: #777777; + padding: 15px 15px 15px 25px; + } + + .side-nav>li.dropdown>ul.dropdown-menu>li>a:hover, + .side-nav>li.dropdown>ul.dropdown-menu>li>a.active, + .side-nav>li.dropdown>ul.dropdown-menu>li>a:focus { + background-color: #ffffff; + } + + .side-nav>li>a { + width: 225px; + } + + .navbar-default .navbar-nav.side-nav>li>a:hover, + .navbar-default .navbar-nav.side-nav>li>a:focus { + background-color: #ffffff; + } + + /* Nav Messages */ + + .messages-dropdown .dropdown-menu { + min-width: 300px; + } + + .messages-dropdown .dropdown-menu li a { + white-space: normal; + } + + .navbar-collapse { + padding-left: 15px !important; + padding-right: 15px !important; + } + +} diff --git a/apps/workbench/app/assets/stylesheets/select_modal.css.scss b/apps/workbench/app/assets/stylesheets/select_modal.css.scss new file mode 100644 index 0000000000..6fe56515aa --- /dev/null +++ b/apps/workbench/app/assets/stylesheets/select_modal.css.scss @@ -0,0 +1,3 @@ +.selectable.active, .selectable:hover { + background: #d9edf7; +} diff --git a/apps/workbench/app/assets/stylesheets/selection.css b/apps/workbench/app/assets/stylesheets/selection.css index 147d6fe93b..5e0da41b7a 100644 --- a/apps/workbench/app/assets/stylesheets/selection.css +++ b/apps/workbench/app/assets/stylesheets/selection.css @@ -2,18 +2,8 @@ width: 500px; } -#selection-form-content > li > a, #selection-form-content > li > input { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.42857; - color: rgb(51, 51, 51); - white-space: nowrap; - border: none; - background: transparent; - width: 100%; - text-align: left; +#selection-form-content > li > a, #selection-form-content > li > button { + margin: 3px 20px; } #selection-form-content li table tr { @@ -22,8 +12,6 @@ border-top: 1px solid rgb(221, 221, 221); } -#selection-form-content a:hover, #selection-form-content a:focus, #selection-form-content input:hover, #selection-form-content input:focus, #selection-form-content tr:hover { - text-decoration: none; - color: rgb(38, 38, 38); - background-color: whitesmoke; -} \ No newline at end of file +#selection-form-content li table tr:last-child { + border-bottom: 1px solid rgb(221, 221, 221); +} diff --git a/apps/workbench/app/controllers/actions_controller.rb b/apps/workbench/app/controllers/actions_controller.rb index 8a817f03cd..368d9a8e8c 100644 --- a/apps/workbench/app/controllers/actions_controller.rb +++ b/apps/workbench/app/controllers/actions_controller.rb @@ -1,8 +1,39 @@ class ActionsController < ApplicationController - skip_before_filter :find_object_by_uuid, only: :post + @@exposed_actions = {} + def self.expose_action method, &block + @@exposed_actions[method] = true + define_method method, block + end + + def model_class + ArvadosBase::resource_class_for_uuid(params[:uuid]) + end + + def post + params.keys.collect(&:to_sym).each do |param| + if @@exposed_actions[param] + return self.send(param) + end + end + redirect_to :back + end + + expose_action :copy_selections_into_folder do + already_named = Link. + filter([['tail_uuid','=',@object.uuid], + ['head_uuid','in',params["selection"]]]). + 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}") + end + redirect_to @object + end - def combine_selected_files_into_collection + expose_action :combine_selected_files_into_collection do lst = [] files = [] params["selection"].each do |s| @@ -55,7 +86,7 @@ class ActionsController < ApplicationController env = Hash[ENV]. merge({ 'ARVADOS_API_HOST' => - $arvados_api_client.arvados_v1_base. + arvados_api_client.arvados_v1_base. sub(/\/arvados\/v1/, ''). sub(/^https?:\/\//, ''), 'ARVADOS_API_TOKEN' => Thread.current[:arvados_api_token], @@ -87,11 +118,4 @@ class ActionsController < ApplicationController redirect_to controller: 'collections', action: :show, id: newc.uuid end - def post - if params["combine_selected_files_into_collection"] - combine_selected_files_into_collection - else - redirect_to :back - end - end end diff --git a/apps/workbench/app/controllers/api_client_authorizations_controller.rb b/apps/workbench/app/controllers/api_client_authorizations_controller.rb index 8385b6b2d0..85f52f20ab 100644 --- a/apps/workbench/app/controllers/api_client_authorizations_controller.rb +++ b/apps/workbench/app/controllers/api_client_authorizations_controller.rb @@ -1,17 +1,4 @@ class ApiClientAuthorizationsController < ApplicationController - def index - m = model_class.all - items_available = m.items_available - offset = m.result_offset - limit = m.result_limit - filtered = m.to_ary.reject do |x| - x.api_client_id == 0 or (x.expires_at and x.expires_at < Time.now) rescue false - end - ArvadosApiClient::patch_paging_vars(filtered, items_available, offset, limit, nil) - @objects = ArvadosResourceList.new(ApiClientAuthorization) - @objects.results= filtered - super - end def index_pane_list %w(Recent Help) diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb index f9de62d60e..59ca3503c8 100644 --- a/apps/workbench/app/controllers/application_controller.rb +++ b/apps/workbench/app/controllers/application_controller.rb @@ -1,16 +1,16 @@ class ApplicationController < ActionController::Base + include ArvadosApiClientHelper + respond_to :html, :json, :js protect_from_forgery ERROR_ACTIONS = [:render_error, :render_not_found] around_filter :thread_clear - around_filter(:thread_with_mandatory_api_token, - except: [:index, :show] + ERROR_ACTIONS) + around_filter :thread_with_mandatory_api_token, except: ERROR_ACTIONS around_filter :thread_with_optional_api_token before_filter :check_user_agreements, except: ERROR_ACTIONS before_filter :check_user_notifications, except: ERROR_ACTIONS - around_filter :using_reader_tokens, only: [:index, :show] before_filter :find_object_by_uuid, except: [:index] + ERROR_ACTIONS theme :select_theme @@ -64,19 +64,27 @@ class ApplicationController < ActionController::Base end def index + @limit ||= 200 if params[:limit] - limit = params[:limit].to_i - else - limit = 200 + @limit = params[:limit].to_i end + @offset ||= 0 if params[:offset] - offset = params[:offset].to_i - else - offset = 0 + @offset = params[:offset].to_i + end + + @filters ||= [] + if params[:filters] + filters = params[:filters] + if filters.is_a? String + filters = Oj.load filters + end + @filters += filters end - @objects ||= model_class.limit(limit).offset(offset).all + @objects ||= model_class + @objects = @objects.filter(@filters).limit(@limit).offset(@offset).all respond_to do |f| f.json { render json: @objects } f.html { render } @@ -89,7 +97,7 @@ class ApplicationController < ActionController::Base return render_not_found("object not found") end respond_to do |f| - f.json { render json: @object } + f.json { render json: @object.attributes.merge(href: url_for(@object)) } f.html { if request.method == 'GET' render @@ -112,21 +120,21 @@ class ApplicationController < ActionController::Base end def update - updates = params[@object.class.to_s.underscore.singularize.to_sym] - updates.keys.each do |attr| + @updates ||= params[@object.class.to_s.underscore.singularize.to_sym] + @updates.keys.each do |attr| if @object.send(attr).is_a? Hash - if updates[attr].is_a? String - updates[attr] = Oj.load updates[attr] + if @updates[attr].is_a? String + @updates[attr] = Oj.load @updates[attr] end if params[:merge] || params["merge_#{attr}".to_sym] # Merge provided Hash with current Hash, instead of # replacing. - updates[attr] = @object.send(attr).with_indifferent_access. - deep_merge(updates[attr].with_indifferent_access) + @updates[attr] = @object.send(attr).with_indifferent_access. + deep_merge(@updates[attr].with_indifferent_access) end end end - if @object.update_attributes updates + if @object.update_attributes @updates show else self.render_error status: 422 @@ -134,16 +142,12 @@ class ApplicationController < ActionController::Base end def create - @object ||= model_class.new params[model_class.to_s.underscore.singularize] + @new_resource_attrs ||= params[model_class.to_s.underscore.singularize] + @new_resource_attrs ||= {} + @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' } + @object ||= model_class.new @new_resource_attrs @object.save! - - respond_to do |f| - f.json { render json: @object } - f.html { - redirect_to(params[:return_to] || @object) - } - f.js { render } - end + show end def destroy @@ -193,7 +197,7 @@ class ApplicationController < ActionController::Base respond_to do |f| f.html { if request.method == 'GET' - redirect_to $arvados_api_client.arvados_login_url(return_to: request.url) + 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." redirect_to :back @@ -207,23 +211,6 @@ class ApplicationController < ActionController::Base false # For convenience to return from callbacks end - def using_reader_tokens(login_optional=false) - if params[:reader_tokens].is_a?(Array) and params[:reader_tokens].any? - Thread.current[:reader_tokens] = params[:reader_tokens] - end - begin - yield - rescue ArvadosApiClient::NotLoggedInException - if login_optional - raise - else - return redirect_to_login - end - ensure - Thread.current[:reader_tokens] = nil - end - end - def using_specific_api_token(api_token) start_values = {} [:arvados_api_token, :user].each do |key| @@ -242,8 +229,14 @@ class ApplicationController < ActionController::Base if params[:id] and params[:id].match /\D/ params[:uuid] = params.delete :id end - if params[:uuid].is_a? String - @object = model_class.find(params[:uuid]) + if not model_class + @object = nil + elsif params[:uuid].is_a? String + if params[:uuid].empty? + @object = nil + else + @object = model_class.find(params[:uuid]) + end else @object = model_class.where(uuid: params[:uuid]).first end diff --git a/apps/workbench/app/controllers/collections_controller.rb b/apps/workbench/app/controllers/collections_controller.rb index a64ac11da3..3b4943f588 100644 --- a/apps/workbench/app/controllers/collections_controller.rb +++ b/apps/workbench/app/controllers/collections_controller.rb @@ -1,7 +1,10 @@ class CollectionsController < ApplicationController - skip_around_filter :thread_with_mandatory_api_token, only: [:show_file] - skip_before_filter :find_object_by_uuid, only: [:provenance, :show_file] - skip_before_filter :check_user_agreements, only: [:show_file] + skip_around_filter(:thread_with_mandatory_api_token, + only: [:show_file, :show_file_links]) + skip_before_filter(:find_object_by_uuid, + only: [:provenance, :show_file, :show_file_links]) + + RELATION_LIMIT = 5 def show_pane_list %w(Files Attributes Metadata Provenance_graph Used_by JSON API) @@ -87,13 +90,20 @@ class CollectionsController < ApplicationController @request_url = request.url end + def show_file_links + Thread.current[:reader_tokens] = [params[:reader_token]] + find_object_by_uuid + render layout: false + end + def show_file # We pipe from arv-get to send the file to the user. Before we start it, # we ask the API server if the file actually exists. This serves two # purposes: it lets us return a useful status code for common errors, and # helps us figure out which token to provide to arv-get. coll = nil - usable_token = find_usable_token do + tokens = [Thread.current[:arvados_api_token], params[:reader_token]].compact + usable_token = find_usable_token(tokens) do coll = Collection.find(params[:uuid]) end if usable_token.nil? @@ -112,80 +122,48 @@ class CollectionsController < ApplicationController def show return super if !@object - @provenance = [] - @output2job = {} - @output2colorindex = {} - @sourcedata = {params[:uuid] => {uuid: params[:uuid]}} - @protected = {} - - colorindex = -1 - any_hope_left = true - while any_hope_left - any_hope_left = false - Job.where(output: @sourcedata.keys).sort_by { |a| a.finished_at || a.created_at }.reverse.each do |job| - if !@output2colorindex[job.output] - any_hope_left = true - @output2colorindex[job.output] = (colorindex += 1) % 10 - @provenance << {job: job, output: job.output} - @sourcedata.delete job.output - @output2job[job.output] = job - job.dependencies.each do |new_source_data| - unless @output2colorindex[new_source_data] - @sourcedata[new_source_data] = {uuid: new_source_data} - end - end - end - end - end - - Link.where(head_uuid: @sourcedata.keys | @output2job.keys).each do |link| - if link.link_class == 'resources' and link.name == 'wants' - @protected[link.head_uuid] = true - if link.tail_uuid == current_user.uuid - @is_persistent = true - end - end - end - Link.where(tail_uuid: @sourcedata.keys).each do |link| - if link.link_class == 'data_origin' - @sourcedata[link.tail_uuid][:data_origins] ||= [] - @sourcedata[link.tail_uuid][:data_origins] << [link.name, link.head_uuid] + if current_user + jobs_with = lambda do |conds| + Job.limit(RELATION_LIMIT).where(conds) + .results.sort_by { |j| j.finished_at || j.created_at } end - end - Collection.where(uuid: @sourcedata.keys).each do |collection| - if @sourcedata[collection.uuid] - @sourcedata[collection.uuid][:collection] = collection - end - end - - Collection.where(uuid: @object.uuid).each do |u| - puts request - @prov_svg = ProvenanceHelper::create_provenance_graph(u.provenance, "provenance_svg", - {:request => request, - :direction => :bottom_up, - :combine_jobs => :script_only}) rescue nil - @used_by_svg = ProvenanceHelper::create_provenance_graph(u.used_by, "used_by_svg", - {:request => request, - :direction => :top_down, - :combine_jobs => :script_only, - :pdata_only => true}) rescue nil - 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") + .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] } + @permissions = Link.limit(RELATION_LIMIT).order("modified_at DESC") + .where(head_uuid: @object.uuid, link_class: 'permission', + name: 'can_read').results + @logs = Log.limit(RELATION_LIMIT).order("created_at DESC") + .where(object_uuid: @object.uuid).results + @is_persistent = Link.limit(1) + .where(head_uuid: @object.uuid, tail_uuid: current_user.uuid, + link_class: 'resources', name: 'wants') + .results.any? + end + @prov_svg = ProvenanceHelper::create_provenance_graph(@object.provenance, "provenance_svg", + {:request => request, + :direction => :bottom_up, + :combine_jobs => :script_only}) rescue nil + @used_by_svg = ProvenanceHelper::create_provenance_graph(@object.used_by, "used_by_svg", + {:request => request, + :direction => :top_down, + :combine_jobs => :script_only, + :pdata_only => true}) rescue nil end protected - def find_usable_token - # Iterate over every token available to make it the current token and + def find_usable_token(token_list) + # Iterate over every given token to make it the current token and # yield the given block. # If the block succeeds, return the token it used. # Otherwise, render an error response based on the most specific # error we encounter, and return nil. - read_tokens = [Thread.current[:arvados_api_token]].compact - if params[:reader_tokens].is_a? Array - read_tokens += params[:reader_tokens] - end most_specific_error = [401] - read_tokens.each do |api_token| + token_list.each do |api_token| using_specific_api_token(api_token) do begin yield @@ -211,12 +189,9 @@ class CollectionsController < ApplicationController end def file_in_collection?(collection, filename) - def normalized_path(part_list) - File.join(part_list).sub(%r{^\./}, '') - end - target = normalized_path([filename]) + target = CollectionsHelper.file_path(File.split(filename)) collection.files.each do |file_spec| - return true if (normalized_path(file_spec[0, 2]) == target) + return true if (CollectionsHelper.file_path(file_spec) == target) end false end @@ -226,25 +201,24 @@ class CollectionsController < ApplicationController end class FileStreamer + include ArvadosApiClientHelper def initialize(opts={}) @opts = opts end def each return unless @opts[:uuid] && @opts[:file] - env = Hash[ENV]. - merge({ - 'ARVADOS_API_HOST' => - $arvados_api_client.arvados_v1_base. - sub(/\/arvados\/v1/, ''). - sub(/^https?:\/\//, ''), - 'ARVADOS_API_TOKEN' => - @opts[:arvados_api_token], - 'ARVADOS_API_HOST_INSECURE' => - Rails.configuration.arvados_insecure_https ? 'true' : 'false' - }) + + env = Hash[ENV].dup + + require 'uri' + u = URI.parse(arvados_api_client.arvados_v1_base) + env['ARVADOS_API_HOST'] = "#{u.host}:#{u.port}" + env['ARVADOS_API_TOKEN'] = @opts[:arvados_api_token] + env['ARVADOS_API_HOST_INSECURE'] = "true" if Rails.configuration.arvados_insecure_https + IO.popen([env, 'arv-get', "#{@opts[:uuid]}/#{@opts[:file]}"], 'rb') do |io| - while buf = io.read(2**20) + while buf = io.read(2**16) yield buf end end diff --git a/apps/workbench/app/controllers/folders_controller.rb b/apps/workbench/app/controllers/folders_controller.rb new file mode 100644 index 0000000000..8ebb1a34b1 --- /dev/null +++ b/apps/workbench/app/controllers/folders_controller.rb @@ -0,0 +1,118 @@ +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 diff --git a/apps/workbench/app/controllers/groups_controller.rb b/apps/workbench/app/controllers/groups_controller.rb index b360b19aae..854496a56a 100644 --- a/apps/workbench/app/controllers/groups_controller.rb +++ b/apps/workbench/app/controllers/groups_controller.rb @@ -1,8 +1,13 @@ class GroupsController < ApplicationController def index - @groups = Group.all + @groups = Group.filter [['group_class', 'not in', ['folder']]] @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 end + + def show + return redirect_to(folder_path(@object)) if @object.group_class == 'folder' + super + end end diff --git a/apps/workbench/app/controllers/jobs_controller.rb b/apps/workbench/app/controllers/jobs_controller.rb index 4705bb5204..4746635c72 100644 --- a/apps/workbench/app/controllers/jobs_controller.rb +++ b/apps/workbench/app/controllers/jobs_controller.rb @@ -23,10 +23,11 @@ class JobsController < ApplicationController def index @svg = "" if params[:uuid] - @jobs = Job.where(uuid: params[:uuid]) - generate_provenance(@jobs) + @objects = Job.where(uuid: params[:uuid]) + generate_provenance(@objects) else - @jobs = Job.all + @limit = 20 + super end end diff --git a/apps/workbench/app/controllers/keep_disks_controller.rb b/apps/workbench/app/controllers/keep_disks_controller.rb index cc89228832..f57455b37f 100644 --- a/apps/workbench/app/controllers/keep_disks_controller.rb +++ b/apps/workbench/app/controllers/keep_disks_controller.rb @@ -4,4 +4,51 @@ class KeepDisksController < ApplicationController @object = KeepDisk.new defaults.merge(params[:keep_disk] || {}) super end + + def index + # Retrieve cache age histogram info from logs. + + # In the logs we expect to find it in an ordered list with entries + # of the form (mtime, disk proportion free). + + # An entry of the form (1388747781, 0.52) means that if we deleted + # the oldest non-presisted blocks until we had 52% of the disk + # free, then all blocks with an mtime greater than 1388747781 + # would be preserved. + + # The chart we want to produce, will tell us how much of the disk + # will be free if we use a cache age of x days. Therefore we will + # produce output specifying the age, cache and persisted. age is + # specified in milliseconds. cache is the size of the cache if we + # delete all blocks older than age. persistent is the size of the + # persisted blocks. It is constant regardless of age, but it lets + # us show a stacked graph. + + # Finally each entry in cache_age_histogram is a dictionary, + # because that's what our charting package wats. + + @cache_age_histogram = [] + @histogram_pretty_date = nil + histogram_log = Log. + filter([[:event_type, '=', 'block-age-free-space-histogram']]). + order(:created_at => :desc). + limit(1) + histogram_log.each do |log_entry| + # We expect this block to only execute at most once since we + # specified limit(1) + @cache_age_histogram = log_entry['properties'][:histogram] + # Javascript wants dates in milliseconds. + histogram_date_ms = log_entry['event_at'].to_i * 1000 + @histogram_pretty_date = log_entry['event_at'].strftime('%b %-d, %Y') + + total_free_cache = @cache_age_histogram[-1][1] + persisted_storage = 1 - total_free_cache + @cache_age_histogram.map! { |x| {:age => histogram_date_ms - x[0]*1000, + :cache => total_free_cache - x[1], + :persisted => persisted_storage} } + end + + # Do the regular control work needed. + super + end end diff --git a/apps/workbench/app/controllers/keep_services_controller.rb b/apps/workbench/app/controllers/keep_services_controller.rb new file mode 100644 index 0000000000..eac2e226a8 --- /dev/null +++ b/apps/workbench/app/controllers/keep_services_controller.rb @@ -0,0 +1,2 @@ +class KeepServicesController < ApplicationController +end diff --git a/apps/workbench/app/controllers/pipeline_instances_controller.rb b/apps/workbench/app/controllers/pipeline_instances_controller.rb index 221ed87ad7..761dc91414 100644 --- a/apps/workbench/app/controllers/pipeline_instances_controller.rb +++ b/apps/workbench/app/controllers/pipeline_instances_controller.rb @@ -142,7 +142,11 @@ class PipelineInstancesController < ApplicationController end def show_pane_list - %w(Components Graph Attributes Metadata JSON API) + panes = %w(Components Graph Attributes Metadata JSON API) + if @object and @object.state.in? ['New', 'Ready'] + panes = %w(Inputs) + panes + end + panes end def compare_pane_list @@ -150,7 +154,7 @@ class PipelineInstancesController < ApplicationController end def index - @objects ||= model_class.limit(20).all + @limit = 20 super end diff --git a/apps/workbench/app/controllers/sessions_controller.rb b/apps/workbench/app/controllers/sessions_controller.rb index 488c67c3c2..585f322a85 100644 --- a/apps/workbench/app/controllers/sessions_controller.rb +++ b/apps/workbench/app/controllers/sessions_controller.rb @@ -4,7 +4,7 @@ class SessionsController < ApplicationController skip_before_filter :find_object_by_uuid, :only => [:destroy, :index] def destroy session.clear - redirect_to $arvados_api_client.arvados_logout_url(return_to: root_url) + redirect_to arvados_api_client.arvados_logout_url(return_to: root_url) end def index redirect_to root_url if session[:arvados_api_token] diff --git a/apps/workbench/app/controllers/users_controller.rb b/apps/workbench/app/controllers/users_controller.rb index 863876137f..3d8c8530ad 100644 --- a/apps/workbench/app/controllers/users_controller.rb +++ b/apps/workbench/app/controllers/users_controller.rb @@ -107,11 +107,11 @@ class UsersController < ApplicationController end def sudo - resp = $arvados_api_client.api(ApiClientAuthorization, '', { - api_client_authorization: { - owner_uuid: @object.uuid - } - }) + resp = arvados_api_client.api(ApiClientAuthorization, '', { + api_client_authorization: { + owner_uuid: @object.uuid + } + }) redirect_to root_url(api_token: resp[:api_token]) end diff --git a/apps/workbench/app/helpers/application_helper.rb b/apps/workbench/app/helpers/application_helper.rb index b17231336f..d844350927 100644 --- a/apps/workbench/app/helpers/application_helper.rb +++ b/apps/workbench/app/helpers/application_helper.rb @@ -17,6 +17,7 @@ module ApplicationHelper def human_readable_bytes_html(n) return h(n) unless n.is_a? Fixnum + return "0 bytes" if (n == 0) orders = { 1 => "bytes", @@ -141,16 +142,29 @@ module ApplicationHelper attrvalue = attrvalue.to_json if attrvalue.is_a? Hash or attrvalue.is_a? Array - link_to attrvalue.to_s, '#', { + ajax_options = { + "data-pk" => { + id: object.uuid, + key: object.class.to_s.underscore + } + } + if object.uuid + ajax_options['data-url'] = url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore) + else + ajax_options['data-url'] = url_for(action: "create", controller: object.class.to_s.pluralize.underscore) + ajax_options['data-pk'][:defaults] = object.attributes + end + ajax_options['data-pk'] = ajax_options['data-pk'].to_json + + content_tag 'span', attrvalue.to_s, { "data-emptytext" => "none", "data-placement" => "bottom", "data-type" => input_type, - "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore), "data-title" => "Update #{attr.gsub '_', ' '}", "data-name" => attr, - "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}", + "data-object-uuid" => object.uuid, :class => "editable" - }.merge(htmloptions) + }.merge(htmloptions).merge(ajax_options) end def render_pipeline_component_attribute(object, attr, subattr, value_info, htmloptions={}) @@ -254,7 +268,7 @@ module ApplicationHelper "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}", "data-showbuttons" => "false", "data-value" => attrvalue, - :class => "editable #{'required' if required}", + :class => "editable #{'required' if required} form-control", :id => id }.merge(htmloptions) @@ -264,10 +278,22 @@ module ApplicationHelper lt += raw("add_form_selection_sources(#{selectables.to_json});\n") end - lt += raw("$('##{id}').editable({source: function() { return select_form_sources('#{dataclass}'); } });\n") + lt += raw("$('[data-name=\"#{dn}\"]').editable({source: function() { return select_form_sources('#{dataclass}'); } });\n") lt += raw("") lt end + + def render_arvados_object_list_start(list, button_text, button_href, + params={}, *rest, &block) + show_max = params.delete(:show_max) || 3 + params[:class] ||= 'btn btn-xs btn-default' + list[0...show_max].each { |item| yield item } + unless list[show_max].nil? + link_to(h(button_text) + + raw('   '), + button_href, params, *rest) + end + end end diff --git a/apps/workbench/app/helpers/arvados_api_client_helper.rb b/apps/workbench/app/helpers/arvados_api_client_helper.rb new file mode 100644 index 0000000000..b6c29a9070 --- /dev/null +++ b/apps/workbench/app/helpers/arvados_api_client_helper.rb @@ -0,0 +1,13 @@ +module ArvadosApiClientHelper + def arvados_api_client + ArvadosApiClient.new_or_current + end +end + +# For the benefit of themes that still expect $arvados_api_client to work: +class ArvadosClientProxyHack + def method_missing *args + ArvadosApiClient.new_or_current.send *args + end +end +$arvados_api_client = ArvadosClientProxyHack.new diff --git a/apps/workbench/app/helpers/folders_helper.rb b/apps/workbench/app/helpers/folders_helper.rb new file mode 100644 index 0000000000..d27e7b4fb6 --- /dev/null +++ b/apps/workbench/app/helpers/folders_helper.rb @@ -0,0 +1,2 @@ +module FoldersHelper +end diff --git a/apps/workbench/app/helpers/pipeline_instances_helper.rb b/apps/workbench/app/helpers/pipeline_instances_helper.rb index bb0ff74c34..7b6fb72763 100644 --- a/apps/workbench/app/helpers/pipeline_instances_helper.rb +++ b/apps/workbench/app/helpers/pipeline_instances_helper.rb @@ -22,6 +22,26 @@ module PipelineInstancesHelper pj end + def pipeline_log_history(job_uuids) + results = [] + + log_history = Log.where(event_type: 'stderr', + object_uuid: job_uuids).order('id DESC') + if !log_history.results.empty? + reversed_results = log_history.results.reverse + reversed_results.each do |entry| + if entry.andand.properties + properties = entry.properties + text = properties[:text] + if text + results = results.concat text.split("\n") + end + end + end + end + + return results + end protected @@ -30,7 +50,6 @@ module PipelineInstancesHelper i = -1 object.components.each do |cname, c| - puts cname, c i += 1 pj = {index: i, name: cname} pj[:job] = c[:job].is_a?(Hash) ? c[:job] : {} diff --git a/apps/workbench/app/models/arvados_api_client.rb b/apps/workbench/app/models/arvados_api_client.rb index c7f7d3435e..a7ae8ba3aa 100644 --- a/apps/workbench/app/models/arvados_api_client.rb +++ b/apps/workbench/app/models/arvados_api_client.rb @@ -7,21 +7,38 @@ class ArvadosApiClient class InvalidApiResponseException < StandardError end - @@client_mtx = Mutex.new - @@api_client = nil @@profiling_enabled = Rails.configuration.profiling_enabled + @@discovery = nil + + # An API client object suitable for handling API requests on behalf + # of the current thread. + def self.new_or_current + # If this thread doesn't have an API client yet, *or* this model + # has been reloaded since the existing client was created, create + # a new client. Otherwise, keep using the latest client created in + # the current thread. + unless Thread.current[:arvados_api_client].andand.class == self + Thread.current[:arvados_api_client] = new + end + Thread.current[:arvados_api_client] + end + + def initialize *args + @api_client = nil + @client_mtx = Mutex.new + end def api(resources_kind, action, data=nil) profile_checkpoint - @@client_mtx.synchronize do - if not @@api_client - @@api_client = HTTPClient.new + if not @api_client + @client_mtx.synchronize do + @api_client = HTTPClient.new if Rails.configuration.arvados_insecure_https - @@api_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE + @api_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE else # Use system CA certificates - @@api_client.ssl_config.add_trust_ca('/etc/ssl/certs') + @api_client.ssl_config.add_trust_ca('/etc/ssl/certs') end end end @@ -57,10 +74,12 @@ class ArvadosApiClient header = {"Accept" => "application/json"} - profile_checkpoint { "Prepare request #{url} #{query[:uuid]} #{query[:where]}" } - msg = @@api_client.post(url, - query, - header: header) + profile_checkpoint { "Prepare request #{url} #{query[:uuid]} #{query[:where]} #{query[:filters]}" } + msg = @client_mtx.synchronize do + @api_client.post(url, + query, + header: header) + end profile_checkpoint 'API transaction' if msg.status_code == 401 @@ -158,7 +177,7 @@ class ArvadosApiClient end def discovery - @discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', '' + @@discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', '' end def kind_class(kind) diff --git a/apps/workbench/app/models/arvados_base.rb b/apps/workbench/app/models/arvados_base.rb index 1cf0d1fe84..2c2963ca59 100644 --- a/apps/workbench/app/models/arvados_base.rb +++ b/apps/workbench/app/models/arvados_base.rb @@ -2,11 +2,19 @@ class ArvadosBase < ActiveRecord::Base self.abstract_class = true attr_accessor :attribute_sortkey + def self.arvados_api_client + ArvadosApiClient.new_or_current + end + + def arvados_api_client + ArvadosApiClient.new_or_current + end + def self.uuid_infix_object_kind @@uuid_infix_object_kind ||= begin infix_kind = {} - $arvados_api_client.discovery[:schemas].each do |name, schema| + arvados_api_client.discovery[:schemas].each do |name, schema| if schema[:uuidPrefix] infix_kind[schema[:uuidPrefix]] = 'arvados#' + name.to_s.camelcase(:lower) @@ -21,21 +29,27 @@ class ArvadosBase < ActiveRecord::Base end end - def initialize(*args) - super(*args) + def initialize raw_params={} + super self.class.permit_attribute_params(raw_params) @attribute_sortkey ||= { 'id' => nil, - 'uuid' => '000', - 'owner_uuid' => '001', - 'created_at' => '002', - 'modified_at' => '003', - 'modified_by_user_uuid' => '004', - 'modified_by_client_uuid' => '005', - 'name' => '050', - 'tail_uuid' => '100', - 'head_uuid' => '101', - 'info' => 'zzz-000', - 'updated_at' => 'zzz-999' + 'name' => '000', + 'owner_uuid' => '002', + 'event_type' => '100', + 'link_class' => '100', + 'group_class' => '100', + 'tail_uuid' => '101', + 'head_uuid' => '102', + 'object_uuid' => '102', + 'summary' => '104', + 'description' => '104', + 'properties' => '150', + 'info' => '150', + 'created_at' => '200', + 'modified_at' => '201', + 'modified_by_user_uuid' => '202', + 'modified_by_client_uuid' => '203', + 'uuid' => '999', } end @@ -43,7 +57,7 @@ class ArvadosBase < ActiveRecord::Base return @columns unless @columns.nil? @columns = [] @attribute_info ||= {} - schema = $arvados_api_client.discovery[:schemas][self.to_s.to_sym] + schema = arvados_api_client.discovery[:schemas][self.to_s.to_sym] return @columns if schema.nil? schema[:properties].each do |k, coldef| case k @@ -58,7 +72,6 @@ class ArvadosBase < ActiveRecord::Base @columns << column(k, :text) serialize k, coldef[:type].constantize end - attr_accessible k @attribute_info[k] = coldef end end @@ -79,14 +92,19 @@ class ArvadosBase < ActiveRecord::Base raise 'argument to find() must be a uuid string. Acceptable formats: warehouse locator or string with format xxxxx-xxxxx-xxxxxxxxxxxxxxx' end + if self == ArvadosBase + # Determine type from uuid and defer to the appropriate subclass. + return resource_class_for_uuid(uuid).find(uuid, opts) + end + # Only do one lookup on the API side per {class, uuid, workbench # request} unless {cache: false} is given via opts. cache_key = "request_#{Thread.current.object_id}_#{self.to_s}_#{uuid}" if opts[:cache] == false - Rails.cache.write cache_key, $arvados_api_client.api(self, '/' + uuid) + Rails.cache.write cache_key, arvados_api_client.api(self, '/' + uuid) end hash = Rails.cache.fetch cache_key do - $arvados_api_client.api(self, '/' + uuid) + arvados_api_client.api(self, '/' + uuid) end new.private_reload(hash) end @@ -115,6 +133,25 @@ class ArvadosBase < ActiveRecord::Base ArvadosResourceList.new(self).all(*args) end + def self.permit_attribute_params raw_params + # strong_parameters does not provide security in Workbench: anyone + # who can get this far can just as well do a call directly to our + # database (Arvados) with the same credentials we use. + # + # The following permit! is necessary even with + # "ActionController::Parameters.permit_all_parameters = true", + # because permit_all does not permit nested attributes. + ActionController::Parameters.new(raw_params).permit! + end + + def self.create raw_params={} + super(permit_attribute_params(raw_params)) + end + + def update_attributes raw_params={} + super(self.class.permit_attribute_params(raw_params)) + end + def save obdata = {} self.class.columns.each do |col| @@ -125,9 +162,9 @@ class ArvadosBase < ActiveRecord::Base if etag postdata['_method'] = 'PUT' obdata.delete :uuid - resp = $arvados_api_client.api(self.class, '/' + uuid, postdata) + resp = arvados_api_client.api(self.class, '/' + uuid, postdata) else - resp = $arvados_api_client.api(self.class, '', postdata) + resp = arvados_api_client.api(self.class, '', postdata) end return false if !resp[:etag] || !resp[:uuid] @@ -154,7 +191,7 @@ class ArvadosBase < ActiveRecord::Base def destroy if etag || uuid postdata = { '_method' => 'DELETE' } - resp = $arvados_api_client.api(self.class, '/' + uuid, postdata) + resp = arvados_api_client.api(self.class, '/' + uuid, postdata) resp[:etag] && resp[:uuid] && resp else true @@ -181,13 +218,13 @@ class ArvadosBase < ActiveRecord::Base ok end end - @links = $arvados_api_client.api Link, '', { _method: 'GET', where: o, eager: true } - @links = $arvados_api_client.unpack_api_response(@links) + @links = arvados_api_client.api Link, '', { _method: 'GET', where: o, eager: true } + @links = arvados_api_client.unpack_api_response(@links) end def all_links return @all_links if @all_links - res = $arvados_api_client.api Link, '', { + res = arvados_api_client.api Link, '', { _method: 'GET', where: { tail_kind: self.kind, @@ -195,7 +232,7 @@ class ArvadosBase < ActiveRecord::Base }, eager: true } - @all_links = $arvados_api_client.unpack_api_response(res) + @all_links = arvados_api_client.unpack_api_response(res) end def reload @@ -207,7 +244,7 @@ class ArvadosBase < ActiveRecord::Base if uuid_or_hash.is_a? Hash hash = uuid_or_hash else - hash = $arvados_api_client.api(self.class, '/' + uuid_or_hash) + hash = arvados_api_client.api(self.class, '/' + uuid_or_hash) end hash.each do |k,v| if self.respond_to?(k.to_s + '=') @@ -244,14 +281,24 @@ class ArvadosBase < ActiveRecord::Base } end + def class_for_display + self.class.to_s + end + def self.creatable? current_user end + def self.goes_in_folders? + false + end + def editable? (current_user and current_user.is_active and (current_user.is_admin or - current_user.uuid == self.owner_uuid)) + current_user.uuid == self.owner_uuid or + new_record? or + (writable_by.include? current_user.uuid rescue false))) end def attribute_editable?(attr) @@ -259,10 +306,10 @@ class ArvadosBase < ActiveRecord::Base false elsif not (current_user.andand.is_active) false - elsif "uuid owner_uuid".index(attr.to_s) or current_user.is_admin + elsif attr == 'uuid' current_user.is_admin else - current_user.uuid == self.owner_uuid or current_user.uuid == self.uuid + editable? end end @@ -281,13 +328,13 @@ class ArvadosBase < ActiveRecord::Base end resource_class = nil uuid.match /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/ do |re| - resource_class ||= $arvados_api_client. + resource_class ||= arvados_api_client. kind_class(self.uuid_infix_object_kind[re[1]]) end if opts[:referring_object] and opts[:referring_attr] and opts[:referring_attr].match /_uuid$/ - resource_class ||= $arvados_api_client. + resource_class ||= arvados_api_client. kind_class(opts[:referring_object]. attributes[opts[:referring_attr]. sub(/_uuid$/, '_kind')]) @@ -299,10 +346,18 @@ class ArvadosBase < ActiveRecord::Base (name if self.respond_to? :name) || uuid end + def content_summary + self.class_for_display + end + def selection_label friendly_link_name end + def owner + ArvadosBase.find(owner_uuid) rescue nil + end + protected def forget_uuid! diff --git a/apps/workbench/app/models/arvados_resource_list.rb b/apps/workbench/app/models/arvados_resource_list.rb index 258aa44e2e..dedd18c81d 100644 --- a/apps/workbench/app/models/arvados_resource_list.rb +++ b/apps/workbench/app/models/arvados_resource_list.rb @@ -1,4 +1,5 @@ class ArvadosResourceList + include ArvadosApiClientHelper include Enumerable def initialize resource_class=nil @@ -53,7 +54,7 @@ class ArvadosResourceList end cond.keys.select { |x| x.match /_kind$/ }.each do |kind_key| if cond[kind_key].is_a? Class - cond = cond.merge({ kind_key => 'arvados#' + $arvados_api_client.class_kind(cond[kind_key]) }) + cond = cond.merge({ kind_key => 'arvados#' + arvados_api_client.class_kind(cond[kind_key]) }) end end api_params = { @@ -65,8 +66,8 @@ class ArvadosResourceList api_params[:offset] = @offset if @offset api_params[:order] = @orderby_spec if @orderby_spec api_params[:filters] = @filters if @filters - res = $arvados_api_client.api @resource_class, '', api_params - @results = $arvados_api_client.unpack_api_response res + res = arvados_api_client.api @resource_class, '', api_params + @results = arvados_api_client.unpack_api_response res self end @@ -90,6 +91,12 @@ class ArvadosResourceList self end + def collect + results.collect do |m| + yield m + end + end + def first results.first end diff --git a/apps/workbench/app/models/collection.rb b/apps/workbench/app/models/collection.rb index 5460e9a6e0..2fe4e2b748 100644 --- a/apps/workbench/app/models/collection.rb +++ b/apps/workbench/app/models/collection.rb @@ -1,4 +1,5 @@ class Collection < ArvadosBase + include ApplicationHelper MD5_EMPTY = 'd41d8cd98f00b204e9800998ecf8427e' @@ -7,6 +8,14 @@ class Collection < ArvadosBase !!locator.to_s.match("^#{MD5_EMPTY}(\\+.*)?\$") end + def self.goes_in_folders? + true + end + + def content_summary + human_readable_bytes_html(total_bytes) + " " + super + end + def total_bytes if files tot = 0 @@ -17,6 +26,27 @@ class Collection < ArvadosBase end end + def files_tree + 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| + until tree.include?(splitdir) + tree[splitdir] = [] + splitdir = File.split(splitdir.first) + end + end + dir_to_tree = lambda do |dirname| + # First list subdirectories, with their files inside. + subnodes = tree.keys.select { |bd, td| (bd == dirname) and (td != '.') } + .sort.flat_map do |parts| + [parts + [nil]] + dir_to_tree.call(File.join(parts)) + end + # Then extend that list with files in this directory. + subnodes + tree[File.split(dirname)] + end + dir_to_tree.call('.') + end + def attribute_editable?(attr) false end @@ -26,11 +56,11 @@ class Collection < ArvadosBase end def provenance - $arvados_api_client.api "collections/#{self.uuid}/", "provenance" + arvados_api_client.api "collections/#{self.uuid}/", "provenance" end def used_by - $arvados_api_client.api "collections/#{self.uuid}/", "used_by" + arvados_api_client.api "collections/#{self.uuid}/", "used_by" end end diff --git a/apps/workbench/app/models/group.rb b/apps/workbench/app/models/group.rb index c628b5699c..638f6e884a 100644 --- a/apps/workbench/app/models/group.rb +++ b/apps/workbench/app/models/group.rb @@ -1,10 +1,24 @@ class Group < ArvadosBase + def self.goes_in_folders? + true + end + def contents params={} - res = $arvados_api_client.api self.class, "/#{self.uuid}/contents", { + res = arvados_api_client.api self.class, "/#{self.uuid}/contents", { _method: 'GET' }.merge(params) ret = ArvadosResourceList.new - ret.results = $arvados_api_client.unpack_api_response(res) + ret.results = arvados_api_client.unpack_api_response(res) ret end + + def class_for_display + group_class == 'folder' ? 'Folder' : super + end + + def editable? + respond_to?(:writable_by) and + writable_by and + writable_by.index(current_user.uuid) + end end diff --git a/apps/workbench/app/models/human.rb b/apps/workbench/app/models/human.rb index 31653bd6c7..3880f0513d 100644 --- a/apps/workbench/app/models/human.rb +++ b/apps/workbench/app/models/human.rb @@ -1,2 +1,5 @@ class Human < ArvadosBase + def self.goes_in_folders? + true + end end diff --git a/apps/workbench/app/models/job.rb b/apps/workbench/app/models/job.rb index f88834e0c3..56428abb2c 100644 --- a/apps/workbench/app/models/job.rb +++ b/apps/workbench/app/models/job.rb @@ -1,5 +1,13 @@ class Job < ArvadosBase + def self.goes_in_folders? + true + end + def attribute_editable?(attr) false end + + def self.creatable? + false + end end diff --git a/apps/workbench/app/models/keep_service.rb b/apps/workbench/app/models/keep_service.rb new file mode 100644 index 0000000000..f27e369b86 --- /dev/null +++ b/apps/workbench/app/models/keep_service.rb @@ -0,0 +1,5 @@ +class KeepService < ArvadosBase + def self.creatable? + current_user and current_user.is_admin + end +end diff --git a/apps/workbench/app/models/log.rb b/apps/workbench/app/models/log.rb index c804bf7b71..39d585bf90 100644 --- a/apps/workbench/app/models/log.rb +++ b/apps/workbench/app/models/log.rb @@ -1,3 +1,8 @@ class Log < ArvadosBase attr_accessor :object + def self.creatable? + # Technically yes, but not worth offering: it will be empty, and + # you won't be able to edit it. + false + end end diff --git a/apps/workbench/app/models/pipeline_instance.rb b/apps/workbench/app/models/pipeline_instance.rb index ccb88351a7..aad7cfc4cc 100644 --- a/apps/workbench/app/models/pipeline_instance.rb +++ b/apps/workbench/app/models/pipeline_instance.rb @@ -1,6 +1,10 @@ class PipelineInstance < ArvadosBase attr_accessor :pipeline_template + def self.goes_in_folders? + true + end + def update_job_parameters(new_params) self.components[:steps].each_with_index do |step, i| step[:params].each do |param| @@ -18,7 +22,8 @@ class PipelineInstance < ArvadosBase end def attribute_editable?(attr) - attr.to_sym == :name || (attr.to_sym == :components and self.active == nil) + attr && (attr.to_sym == :name || + (attr.to_sym == :components and (self.state == 'New' || self.state == 'Ready'))) end def attributes_for_display diff --git a/apps/workbench/app/models/pipeline_template.rb b/apps/workbench/app/models/pipeline_template.rb index fb12a8119d..82e3d307c0 100644 --- a/apps/workbench/app/models/pipeline_template.rb +++ b/apps/workbench/app/models/pipeline_template.rb @@ -1,4 +1,8 @@ class PipelineTemplate < ArvadosBase + def self.goes_in_folders? + true + end + def self.creatable? false end diff --git a/apps/workbench/app/models/specimen.rb b/apps/workbench/app/models/specimen.rb index fa7fbbf090..68c0038b63 100644 --- a/apps/workbench/app/models/specimen.rb +++ b/apps/workbench/app/models/specimen.rb @@ -1,2 +1,5 @@ class Specimen < ArvadosBase + def self.goes_in_folders? + true + end end diff --git a/apps/workbench/app/models/trait.rb b/apps/workbench/app/models/trait.rb index ab65ad59db..cf7099d2fe 100644 --- a/apps/workbench/app/models/trait.rb +++ b/apps/workbench/app/models/trait.rb @@ -1,2 +1,5 @@ class Trait < ArvadosBase + def self.goes_in_folders? + true + end end diff --git a/apps/workbench/app/models/user.rb b/apps/workbench/app/models/user.rb index 44d615b89f..c1656bde69 100644 --- a/apps/workbench/app/models/user.rb +++ b/apps/workbench/app/models/user.rb @@ -6,15 +6,15 @@ class User < ArvadosBase end def self.current - res = $arvados_api_client.api self, '/current' - $arvados_api_client.unpack_api_response(res) + res = arvados_api_client.api self, '/current' + arvados_api_client.unpack_api_response(res) end def self.system - $arvados_system_user ||= begin - res = $arvados_api_client.api self, '/system' - $arvados_api_client.unpack_api_response(res) - end + @@arvados_system_user ||= begin + res = arvados_api_client.api self, '/system' + arvados_api_client.unpack_api_response(res) + end end def full_name @@ -22,9 +22,9 @@ class User < ArvadosBase end def activate - self.private_reload($arvados_api_client.api(self.class, - "/#{self.uuid}/activate", - {})) + self.private_reload(arvados_api_client.api(self.class, + "/#{self.uuid}/activate", + {})) end def attributes_for_display @@ -40,13 +40,13 @@ class User < ArvadosBase end def unsetup - self.private_reload($arvados_api_client.api(self.class, - "/#{self.uuid}/unsetup", - {})) + self.private_reload(arvados_api_client.api(self.class, + "/#{self.uuid}/unsetup", + {})) end def self.setup params - $arvados_api_client.api(self, "/setup", params) + arvados_api_client.api(self, "/setup", params) end end diff --git a/apps/workbench/app/models/user_agreement.rb b/apps/workbench/app/models/user_agreement.rb index 63b845228f..d77038cdd5 100644 --- a/apps/workbench/app/models/user_agreement.rb +++ b/apps/workbench/app/models/user_agreement.rb @@ -1,10 +1,10 @@ class UserAgreement < ArvadosBase def self.signatures - res = $arvados_api_client.api self, '/signatures' - $arvados_api_client.unpack_api_response(res) + res = arvados_api_client.api self, '/signatures' + arvados_api_client.unpack_api_response(res) end def self.sign(params) - res = $arvados_api_client.api self, '/sign', params - $arvados_api_client.unpack_api_response(res) + res = arvados_api_client.api self, '/sign', params + arvados_api_client.unpack_api_response(res) end end diff --git a/apps/workbench/app/views/application/_content.html.erb b/apps/workbench/app/views/application/_content.html.erb index 53444a5c9c..8a0624b7af 100644 --- a/apps/workbench/app/views/application/_content.html.erb +++ b/apps/workbench/app/views/application/_content.html.erb @@ -4,7 +4,7 @@ <% pane_list ||= %w(recent) %> <% panes = Hash[pane_list.map { |pane| [pane, render(partial: 'show_' + pane.downcase, - locals: { comparable: comparable })] + locals: { comparable: comparable, objects: @objects })] }.compact] %>