//= require bootstrap/popover
//= require bootstrap/collapse
//= require bootstrap/modal
+//= require bootstrap/button
//= require bootstrap3-editable/bootstrap-editable
//= require_tree .
return false;
});
+ $(document).
+ on('ajax:complete ready', function() {
+ // See http://getbootstrap.com/javascript/#buttons
+ $('.btn').button();
+ });
+
HeaderRowFixer = function(selector) {
this.duplicateTheadTr = function() {
$(selector).each(function() {
--- /dev/null
+jQuery(function($){
+ $(document).on('click', '.toggle-persist button', 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');
+ $.ajax(toggle_group.attr('data-remote-href'),
+ {dataType: 'json',
+ type: 'POST',
+ data: {
+ value: want_state
+ },
+ context: {
+ toggle_group: toggle_group,
+ want_state: want_state,
+ button: this
+ }
+ }).
+ done(function(data, status, jqxhr) {
+ var context = this;
+ $(document).trigger('ajax:complete');
+ // Remove "danger" status in case a previous action failed
+ $('.btn-danger', context.toggle_group).
+ addClass('btn-info').
+ removeClass('btn-danger');
+ // Update last-saved-state
+ context.toggle_group.
+ attr('data-persistent-state', context.want_state);
+ }).
+ fail(function(jqxhr, status, error) {
+ var context = this;
+ var saved_state;
+ $(document).trigger('ajax:complete');
+ // Add a visual indication that something failed
+ $(context.button).
+ addClass('btn-danger').
+ removeClass('btn-info');
+ // Change to the last-saved-state
+ saved_state = context.toggle_group.attr('data-persistent-state');
+ $(context.button).
+ toggleClass('active', saved_state == 'persistent').
+ html(saved_state == 'persistent' ? 'Persistent' : 'Cache');
+
+ if (jqxhr.readyState == 0 || jqxhr.status == 0) {
+ // Request cancelled due to page reload.
+ // Displaying an alert would be rather annoying.
+ } else if (jqxhr.responseJSON && jqxhr.responseJSON.errors) {
+ window.alert("Request failed: " +
+ jqxhr.responseJSON.errors.join("; "));
+ } else {
+ window.alert("Request failed.");
+ }
+ });
+ $(document).trigger('ajax:send');
+ });
+});
+++ /dev/null
-# 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/
-// Place all the styles related to the Collections controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
+/*
+ "active" and "inactive" colors are too similar for a toggle switch
+ in the default bootstrap theme.
+ */
+
+$inactive-bg: #5bc0de;
+$active-bg: #39b3d7;
+
+.btn-group.toggle-persist .btn {
+ width: 6em;
+}
+.btn-group.toggle-persist .btn-info {
+ background-color: lighten($inactive-bg, 15%);
+}
+
+.btn-group.toggle-persist .btn-info.active {
+ background-color: $active-bg;
+}
%w(Files Attributes Metadata Provenance_graph Used_by JSON API)
end
+ def set_persistent
+ case params[:value]
+ when 'persistent', 'cache'
+ persist_links = Link.filter([['owner_uuid', '=', current_user.uuid],
+ ['link_class', '=', 'resources'],
+ ['name', '=', 'wants'],
+ ['tail_uuid', '=', current_user.uuid],
+ ['head_uuid', '=', @object.uuid]])
+ logger.debug persist_links.inspect
+ else
+ return unprocessable "Invalid value #{value.inspect}"
+ end
+ if params[:value] == 'persistent'
+ if not persist_links.any?
+ Link.create(link_class: 'resources',
+ name: 'wants',
+ tail_uuid: current_user.uuid,
+ head_uuid: @object.uuid)
+ end
+ else
+ persist_links.each do |link|
+ link.destroy || raise
+ end
+ end
+
+ respond_to do |f|
+ f.json { render json: @object }
+ end
+ end
+
def index
if params[:search].andand.length.andand > 0
tags = Link.where(any: ['contains', params[:search]])
self.response_body = FileStreamer.new opts
end
-
def show
return super if !@object
@provenance = []
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|
limit(10).
order('created_at desc').
where(created_by: current_user.uuid)
+ collection_uuids = @my_collections.collect &:uuid
- Link.limit(1000).where(head_uuid: @my_collections.collect(&:uuid),
- link_class: 'tag').each do |link|
- (@my_tag_links[link.head_uuid] ||= []) << link
+ @persist_state = {}
+ collection_uuids.each do |uuid|
+ @persist_state[uuid] = 'cache'
+ end
+
+ Link.limit(1000).filter([['head_uuid', 'in', collection_uuids],
+ ['link_class', 'in', ['tag', 'resources']]]).
+ each do |link|
+ case link.link_class
+ when 'tag'
+ (@my_tag_links[link.head_uuid] ||= []) << link
+ when 'resources'
+ if link.name == 'wants'
+ @persist_state[link.head_uuid] = 'persistent'
+ end
+ end
end
@my_pipelines = PipelineInstance.
order('created_at desc').
where(created_by: current_user.uuid)
-
- # A Tutorial is a Link which has link_class "resources" and name
- # "wants", and is owned by the Tutorials Group (i.e., named
- # "Arvados Tutorials" and owned by the system user).
- @tutorial_group = Group.where(owner_uuid: User.system.uuid,
- name: 'Arvados Tutorials').first
- if @tutorial_group
- @tutorial_links = Link.where(tail_uuid: @tutorial_group.uuid,
- link_class: 'resources',
- name: 'wants')
- else
- @tutorial_links = []
- end
- @tutorial_complete = {
- 'Run a job' => @my_last_job
- }
respond_to do |f|
f.js { render template: 'users/home.js' }
f.html { render template: 'users/home' }
⋮
<% end %>
</td>
- <td><%= link_to_if_arvados_object c.owner_uuid, friendly_name: true %></td>
<td>
<%= raw(distance_of_time_in_words(c.created_at, Time.now).sub('about ','~').sub(' ',' ')) if c.created_at %>
</td>
<td>
- <% if @collection_info[c.uuid][:wanted_by_me] %>
- <span class="label label-info">2×</span>
- <% elsif @collection_info[c.uuid][:wanted] %>
- <span class="label">2×</span>
- <% else %>
- <span class="label">cache</span>
- <% end %>
+ <% current_state = @collection_info[c.uuid][:wanted_by_me] ? 'persistent' : 'cache' %>
+ <%= render partial: 'toggle_persist', locals: { uuid: c.uuid, current_state: current_state } %>
</td>
<td class="add-tag-button">
<a class="btn btn-xs btn-info add-tag-button pull-right" data-remote-href="<%= url_for(controller: 'links', action: 'create') %>" data-remote-method="post"><i class="glyphicon glyphicon-plus"></i> Add</a>
}
<% end %>
+<% content_for :tab_line_buttons do %>
+<div class="row">
+ <div class="col-md-6"></div>
+ <div class="col-md-6">
+ <div class="pull-right">
+ Collection storage status:
+ <%= render partial: 'toggle_persist', locals: { uuid: @object.uuid, current_state: (@is_persistent ? 'persistent' : 'cache') } %>
+ </div>
+ </div>
+</div>
+<% end %>
+
<table class="table table-condensed table-fixedlayout">
<colgroup>
<col width="4%" />
<colgroup>
<col width="4%" />
<col width="10%" />
- <col width="36%" />
+ <col width="34%" />
<col width="15%" />
- <col width="8%" />
- <col width="8%" />
- <col width="23%" />
+ <col width="12%" />
+ <col width="29%" />
</colgroup>
<thead>
<tr class="contain-align-left">
<th></th>
<th>uuid</th>
<th>contents</th>
- <th>owner</th>
<th>age</th>
<th>storage</th>
<th>tags</th>
--- /dev/null
+<div class="btn-group btn-group-xs toggle-persist" data-remote-href="<%= set_persistent_collection_path(id: uuid) %>" data-persistent-state="<%= current_state %>">
+ <button type="button" class="btn btn-info <%= 'active' if current_state == 'persistent' %>"><%= current_state.capitalize %></button>
+</div>
</colgroup>
<tr>
- <th>Script</th>
- <th>Output</th>
- <th>Log</th>
- <th>Age</th>
- <th>Status</th>
- <th>Progress</th>
- </tr>
+ <th>Script</th>
+ <th>Output</th>
+ <th>Log</th>
+ <th>Age</th>
+ <th>Status</th>
+ <th>Progress</th>
+ </tr>
<% @my_jobs[0..6].each do |j| %>
- <tr>
+ <tr data-object-uuid="<%= j.uuid %>">
<td>
<small>
- <%= link_to((j.script.andand[0..31] || j.uuid), job_path(j.uuid)) %>
+ <%= link_to((j.script.andand[0..31] || j.uuid), job_path(j.uuid)) %>
</small>
</td>
- <td>
- <small>
- <% if j.success and j.output %>
+ <td>
+ <small>
+ <% if j.success and j.output %>
- <a href="<%= collection_path(j.output) %>">
- <% Collection.limit(1).where(uuid: j.output).each do |c| %>
- <% c.files.each do |file| %>
- <%= file[0] == '.' ? file[1] : "#{file[0]}/#{file[1]}" %>
- <% end %>
- <% end %>
- </a>
+ <a href="<%= collection_path(j.output) %>">
+ <% Collection.limit(1).where(uuid: j.output).each do |c| %>
+ <% c.files.each do |file| %>
+ <%= file[0] == '.' ? file[1] : "#{file[0]}/#{file[1]}" %>
+ <% end %>
+ <% end %>
+ </a>
- <% end %>
- </small>
+ <% end %>
+ </small>
</td>
<td>
<td>
<small>
- <%= raw(distance_of_time_in_words(j.created_at, Time.now).sub('about
-','~').sub(' ',' ')) if j.created_at %>
+ <%= raw(distance_of_time_in_words(j.created_at, Time.now).sub('about ','~').sub(' ',' ')) if j.created_at %>
</small>
</td>
</colgroup>
<tr>
- <th>Instance</th>
- <th>Template</th>
- <th>Age</th>
- <th>Status</th>
- <th>Progress</th>
+ <th>Instance</th>
+ <th>Template</th>
+ <th>Age</th>
+ <th>Status</th>
+ <th>Progress</th>
</tr>
<% @my_pipelines[0..6].each do |p| %>
- <tr>
+ <tr data-object-uuid="<%= p.uuid %>">
<td>
<small>
- <%= link_to_if_arvados_object p.uuid, friendly_name: true %>
+ <%= link_to_if_arvados_object p.uuid, friendly_name: true %>
</small>
</td>
<td>
<small>
- <%= link_to_if_arvados_object p.pipeline_template_uuid, friendly_name: true %>
+ <%= link_to_if_arvados_object p.pipeline_template_uuid, friendly_name: true %>
</small>
</td>
<td>
<small>
- <%= raw(distance_of_time_in_words(p.created_at, Time.now).sub('about
-','~').sub(' ',' ')) if p.created_at %>
+ <%= raw(distance_of_time_in_words(p.created_at, Time.now).sub('about ','~').sub(' ',' ')) if p.created_at %>
</small>
</td>
<table class="table table-bordered table-condensed table-fixedlayout">
<colgroup>
<col width="46%" />
- <col width="27%" />
- <col width="27%" />
+ <col width="32%" />
+ <col width="10%" />
+ <col width="12%" />
</colgroup>
<tr>
- <th>Contents</th>
- <th>Tags</th>
- <th>Age</th>
+ <th>Contents</th>
+ <th>Tags</th>
+ <th>Age</th>
+ <th>Storage</th>
</tr>
<% @my_collections[0..6].each do |c| %>
- <tr>
+ <tr data-object-uuid="<%= c.uuid %>">
<td>
<small>
- <a href="<%= collection_path(c.uuid) %>">
- <% c.files.each do |file| %>
- <%= file[0] == '.' ? file[1] : "#{file[0]}/#{file[1]}" %>
- <% end %>
- </a>
+ <a href="<%= collection_path(c.uuid) %>">
+ <% c.files.each do |file| %>
+ <%= file[0] == '.' ? file[1] : "#{file[0]}/#{file[1]}" %>
+ <% end %>
+ </a>
</small>
</td>
<td>
</td>
<td>
<small>
- <%= raw(distance_of_time_in_words(c.created_at, Time.now).sub('about
-','~').sub(' ',' ')) if c.created_at %>
+ <%= raw(distance_of_time_in_words(c.created_at, Time.now).sub('about ','~').sub(' ',' ')) if c.created_at %>
</small>
</td>
+ <td>
+ <%= render partial: 'collections/toggle_persist', locals: { uuid: c.uuid, current_state: @persist_state[c.uuid] } %>
+ </td>
</tr>
<% end %>
</table>
<div class="col-sm-8">
<h2>Welcome to Arvados, <%= current_user.first_name %>!</h2>
<div class="well">
- <p>
- Your account must be activated by an Arvados administrator. If this
- is your first time accessing Arvados and would like to request
- access, or you believe you are seeing the page in error, please
- <%= link_to "contact us", Rails.configuration.activation_contact_link %>.
- You should receive an email at the address you used to log in when
- your account is activated. In the mean time, you can
- <%= link_to "learn more about Arvados", "https://arvados.org/projects/arvados/wiki/Introduction_to_Arvados" %>,
- and <%= link_to "read the Arvados user guide", "http://doc.arvados.org/user" %>.
- </p>
- <p style="padding-bottom: 1em">
- <%= link_to raw('Contact us ✉'),
- Rails.configuration.activation_contact_link, class: "pull-right btn btn-primary" %></p>
+ <p>
+ Your account must be activated by an Arvados administrator. If this
+ is your first time accessing Arvados and would like to request
+ access, or you believe you are seeing the page in error, please
+ <%= link_to "contact us", Rails.configuration.activation_contact_link %>.
+ You should receive an email at the address you used to log in when
+ your account is activated. In the mean time, you can
+ <%= link_to "learn more about Arvados", "https://arvados.org/projects/arvados/wiki/Introduction_to_Arvados" %>,
+ and <%= link_to "read the Arvados user guide", "http://doc.arvados.org/user" %>.
+ </p>
+ <p style="padding-bottom: 1em">
+ <%= link_to raw('Contact us ✉'),
+ Rails.configuration.activation_contact_link, class: "pull-right btn btn-primary" %></p>
</div>
</div>
</div>
end
resources :links
match '/collections/graph' => 'collections#graph'
- resources :collections
+ resources :collections do
+ post 'set_persistent', on: :member
+ end
get '/collections/:uuid/*file' => 'collections#show_file', :format => false
post 'actions' => 'actions#post'
--- /dev/null
+require 'integration_helper'
+require 'selenium-webdriver'
+require 'headless'
+
+class CollectionsTest < ActionDispatch::IntegrationTest
+
+ def change_persist oldstate, newstate
+ find "div[data-persistent-state='#{oldstate}']"
+ page.assert_no_selector "div[data-persistent-state='#{newstate}']"
+ find('.btn', text: oldstate.capitalize).click
+ find '.btn', text: newstate.capitalize
+ page.assert_no_selector '.btn', text: oldstate.capitalize
+ find "div[data-persistent-state='#{newstate}']"
+ page.assert_no_selector "div[data-persistent-state='#{oldstate}']"
+ end
+
+ ['/collections', '/'].each do |path|
+ test "Flip persistent switch at #{path}" do
+ Capybara.current_driver = Capybara.javascript_driver
+ uuid = api_fixture('collections')['foo_file']['uuid']
+ visit page_with_token('active', path)
+ within "tr[data-object-uuid='#{uuid}']" do
+ change_persist 'cache', 'persistent'
+ end
+ # Refresh page and make sure the change was committed.
+ visit current_path
+ within "tr[data-object-uuid='#{uuid}']" do
+ change_persist 'persistent', 'cache'
+ end
+ end
+ end
+
+ test 'Flip persistent switch on collection#show' do
+ Capybara.current_driver = Capybara.javascript_driver
+ uuid = api_fixture('collections')['foo_file']['uuid']
+ visit page_with_token('active', "/collections/#{uuid}")
+ change_persist 'cache', 'persistent'
+ visit current_path
+ change_persist 'persistent', 'cache'
+ end
+
+end
@application_version ||= 0.0
@application_name ||= File.split($0).last
- @arvados_api_version = opts[:api_version] ||
- config['ARVADOS_API_VERSION'] ||
- 'v1'
+ @arvados_api_version = opts[:api_version] || 'v1'
+
@arvados_api_host = opts[:api_host] ||
config['ARVADOS_API_HOST'] or
raise "#{$0}: no :api_host or ENV[ARVADOS_API_HOST] provided."
raise "#{$0}: no :api_token or ENV[ARVADOS_API_TOKEN] provided."
if (opts[:suppress_ssl_warnings] or
- config['ARVADOS_API_HOST_INSECURE'])
+ %w(1 true yes).index(config['ARVADOS_API_HOST_INSECURE'].
+ andand.downcase))
suppress_warnings do
OpenSSL::SSL.const_set 'VERIFY_PEER', OpenSSL::SSL::VERIFY_NONE
end
$stderr.puts "#{File.split($0).last} #{$$}: #{message}" if @@debuglevel >= verbosity
end
+ def debuglog *args
+ self.class.debuglog *args
+ end
+
def config(config_file_path="~/.config/arvados/settings.conf")
return @@config if @@config
config['ARVADOS_API_HOST'] = ENV['ARVADOS_API_HOST']
config['ARVADOS_API_TOKEN'] = ENV['ARVADOS_API_TOKEN']
config['ARVADOS_API_HOST_INSECURE'] = ENV['ARVADOS_API_HOST_INSECURE']
- config['ARVADOS_API_VERSION'] = ENV['ARVADOS_API_VERSION']
+
+ if config['ARVADOS_API_HOST'] and config['ARVADOS_API_TOKEN']
+ # Environment variables take precedence over the config file, so
+ # there is no point reading the config file. If the environment
+ # specifies a _HOST without asking for _INSECURE, we certainly
+ # shouldn't give the config file a chance to create a
+ # system-wide _INSECURE state for this user.
+ #
+ # Note: If we start using additional configuration settings from
+ # this file in the future, we might have to read the file anyway
+ # instead of returning here.
+ return (@@config = config)
+ end
begin
expanded_path = File.expand_path config_file_path
# skip comments and blank lines
next if line.match('^\s*#') or not line.match('\S')
var, val = line.chomp.split('=', 2)
+ var.strip!
+ val.strip!
# allow environment settings to override config files.
- if var and val
+ if !var.empty? and val
config[var] ||= val
else
- warn "#{expanded_path}: #{lineno}: could not parse `#{line}'"
+ debuglog "#{expanded_path}: #{lineno}: could not parse `#{line}'", 0
end
end
end
- rescue
- debuglog "HOME environment variable (#{ENV['HOME']}) not set, not using #{config_file_path}", 0
+ rescue StandardError => e
+ debuglog "Ignoring error reading #{config_file_path}: #{e}", 0
end
@@config = config
protect_from_forgery
around_filter :thread_with_auth_info, :except => [:render_error, :render_not_found]
+ before_filter :respond_with_json_by_default
before_filter :remote_ip
before_filter :require_auth_scope, :except => :render_not_found
before_filter :catch_redirect_hint
value[0] == 'contains' then
ilikes = []
model_class.searchable_columns('ilike').each do |column|
+ # Including owner_uuid in an "any column" search will
+ # probably just return a lot of false positives.
+ next if column == 'owner_uuid'
ilikes << "#{ar_table_name}.#{column} ilike ?"
conditions << "%#{value[1]}%"
end
end
# /Authentication
+ def respond_with_json_by_default
+ html_index = request.accepts.index(Mime::HTML)
+ if html_index.nil? or request.accepts[0...html_index].include?(Mime::JSON)
+ request.format = :json
+ end
+ end
+
def model_class
controller_name.classify.constantize
end
def create
@object = Node.new
@object.save!
- @object.start!(lambda { |h| arvados_v1_ping_node_url(h) })
+ @object.start!(lambda { |h| ping_arvados_v1_node_url(h) })
show
end
def self.searchable_columns operator
textonly_operator = !operator.match(/[<=>]/)
self.columns.collect do |col|
- if col.name == 'owner_uuid'
- nil
- elsif [:string, :text].index(col.type)
+ if [:string, :text].index(col.type)
col.name
elsif !textonly_operator and [:datetime, :integer].index(col.type)
col.name
def start!(ping_url_method)
ensure_permission_to_update
- ping_url = ping_url_method.call({ uuid: self.uuid, ping_secret: self.info[:ping_secret] })
+ ping_url = ping_url_method.call({ id: self.uuid, ping_secret: self.info[:ping_secret] })
if (Rails.configuration.compute_node_ec2run_args and
Rails.configuration.compute_node_ami)
ec2_args = ["--user-data '#{ping_url}'",
assert_response 401
end
+ test "create node" do
+ authorize_with :admin
+ post :create
+ assert_response :success
+ assert_not_nil json_response['uuid']
+ assert_not_nil json_response['info'].is_a? Hash
+ assert_not_nil json_response['info']['ping_secret']
+ end
+
end
--- /dev/null
+require 'test_helper'
+
+class LoginWorkflowTest < ActionDispatch::IntegrationTest
+ test "default prompt to login is JSON" do
+ post('/arvados/v1/specimens', {specimen: {}},
+ {'HTTP_ACCEPT' => ''})
+ assert_response 401
+ assert_includes(json_response['errors'], "Not logged in")
+ end
+
+ test "login prompt respects JSON Accept header" do
+ post('/arvados/v1/specimens', {specimen: {}},
+ {'HTTP_ACCEPT' => 'application/json'})
+ assert_response 401
+ assert_includes(json_response['errors'], "Not logged in")
+ end
+
+ test "login prompt respects HTML Accept header" do
+ post('/arvados/v1/specimens', {specimen: {}},
+ {'HTTP_ACCEPT' => 'text/html'})
+ assert_response 302
+ assert_match(%r{/auth/joshid$}, @response.headers['Location'],
+ "HTML login prompt did not include expected redirect")
+ end
+end