gem 'bootstrap-x-editable-rails'
gem 'bootstrap-tab-history-rails'
+gem 'angularjs-rails'
+
gem 'less'
gem 'less-rails'
gem 'wiselinks'
tzinfo (~> 1.1)
addressable (2.3.6)
andand (1.3.3)
+ angularjs-rails (1.3.3)
arel (5.0.1.20140414130214)
arvados (0.1.20141114230720)
activesupport (>= 3.2.13)
DEPENDENCIES
RedCloth
andand
+ angularjs-rails
arvados (>= 0.1.20141114230720)
bootstrap-sass (~> 3.1.0)
bootstrap-tab-history-rails
--- /dev/null
+// Compile any new HTML content that was loaded via jQuery.ajax().
+// Currently this only works for tabs because they emit an
+// arv:pane:loaded event after updating the DOM.
+
+$(document).on('arv:pane:loaded', function(event, updatedElement) {
+ if (updatedElement) {
+ angular.element(updatedElement).injector().invoke(function($compile) {
+ var scope = angular.element(updatedElement).scope();
+ $compile(updatedElement)(scope);
+ });
+ }
+});
//= require bootstrap3-editable/bootstrap-editable
//= require bootstrap-tab-history
//= require wiselinks
+//= require angular
//= require raphael
//= require morris
//= require jquery.number.min
//= require_tree .
jQuery(function($){
- $.ajaxSetup({
- headers: {
- 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
- }
- });
-
$(document).ajaxStart(function(){
$('.modal-with-loading-spinner .spinner').show();
}).ajaxStop(function(){
--- /dev/null
+angular.
+ module('Arvados', []).
+ service('ArvadosClient', ArvadosClient);
+
+ArvadosClient.$inject = ['arvadosApiToken', 'arvadosDiscoveryUri']
+function ArvadosClient(arvadosApiToken, arvadosDiscoveryUri) {
+ $.extend(this, {
+ apiPromise: apiPromise,
+ uniqueNameForManifest: uniqueNameForManifest
+ });
+ return this;
+ ////////////////////////////////
+
+ var that = this;
+ var promiseDiscovery;
+ var discoveryDoc;
+
+ function apiPromise(controller, action, params) {
+ // Start an API call. Return a promise that will resolve with
+ // the API response.
+ return getDiscoveryDoc().then(function() {
+ var meth = discoveryDoc.resources[controller].methods[action];
+ var data = $.extend({}, params, {_method: meth.httpMethod});
+ $.each(data, function(k, v) {
+ if (typeof(v) == 'object') {
+ data[k] = JSON.stringify(v);
+ }
+ });
+ var path = meth.path.replace(/{(.*?)}/, function(_, key) {
+ var val = data[key];
+ delete data[key];
+ return encodeURIComponent(val);
+ });
+ return $.ajax({
+ url: discoveryDoc.baseUrl + path,
+ type: 'POST',
+ crossDomain: true,
+ dataType: 'json',
+ data: data,
+ headers: {
+ Authorization: 'OAuth2 ' + arvadosApiToken
+ }
+ });
+ });
+ }
+
+ function uniqueNameForManifest(manifest, streamName, origName) {
+ // Return an (escaped) filename starting with (unescaped)
+ // origName that won't conflict with any existing names in
+ // the manifest if saved under streamName. streamName must
+ // be exactly as given in the manifest, e.g., "." or
+ // "./foo" or "./foo/bar".
+ //
+ // Example:
+ //
+ // unique('./foo [...] 0:0:bar\040baz\n', '.', 'foo/bar baz')
+ // =>
+ // 'foo/bar\\040baz\\040(1)'
+ var newName;
+ var nameStub = origName;
+ var suffixInt = null;
+ var ok = false;
+ while (!ok) {
+ ok = true;
+ // Add ' (N)' before the filename extension, if any.
+ newName = (!suffixInt ? nameStub :
+ nameStub.replace(/(\.[^.]*)?$/, ' ('+suffixInt+')$1')).
+ replace(/ /g, '\\040');
+ $.each(manifest.split('\n'), function(_, line) {
+ var i, match, foundName;
+ var toks = line.split(' ');
+ for (var i=1; i<toks.length && ok; i++)
+ if (match = toks[i].match(/^\d+:\d+:(\S+)/))
+ if (toks[0] + '/' + match[1] === streamName + '/' + newName) {
+ suffixInt = (suffixInt || 0) + 1;
+ ok = false;
+ }
+ });
+ }
+ return newName;
+ }
+
+ function getDiscoveryDoc() {
+ if (!promiseDiscovery) {
+ promiseDiscovery = $.ajax({
+ url: arvadosDiscoveryUri,
+ crossDomain: true
+ }).then(function(data, status, xhr) {
+ discoveryDoc = data;
+ });
+ }
+ return promiseDiscovery;
+ }
+}
$pane.removeClass('pane-loading');
$pane.addClass('pane-loaded');
$pane.attr('data-loaded-at', (new Date()).getTime());
- $pane.trigger('arv:pane:loaded');
+ $pane.trigger('arv:pane:loaded', $pane);
if ($pane.hasClass('pane-stale')) {
$pane.trigger('arv:pane:reload');
--- /dev/null
+var app = angular.module('Workbench', ['Arvados']);
+app.controller('UploadToCollection', UploadToCollection);
+app.directive('arvUuid', arvUuid);
+
+function arvUuid() {
+ // Copy the given uuid into the current $scope.
+ return {
+ restrict: 'A',
+ link: function(scope, element, attributes) {
+ scope.uuid = attributes.arvUuid;
+ }
+ };
+}
+
+UploadToCollection.$inject = ['$scope', '$filter', '$q', '$timeout',
+ 'ArvadosClient', 'arvadosApiToken'];
+function UploadToCollection($scope, $filter, $q, $timeout,
+ ArvadosClient, arvadosApiToken) {
+ $.extend($scope, {
+ uploadQueue: [],
+ uploader: new QueueUploader(),
+ addFilesToQueue: function(files) {
+ // Angular binding doesn't work its usual magic for file
+ // inputs, so we need to $scope.$apply() this update.
+ $scope.$apply(function(){
+ var i;
+ var insertAt;
+ for (insertAt=0; (insertAt<$scope.uploadQueue.length &&
+ $scope.uploadQueue[insertAt].state != 'Done');
+ insertAt++);
+ for (i=0; i<files.length; i++) {
+ $scope.uploadQueue.splice(insertAt+i, 0,
+ new FileUploader(files[i]));
+ }
+ });
+ },
+ go: function() {
+ $scope.uploader.go();
+ },
+ stop: function() {
+ $scope.uploader.stop();
+ },
+ removeFileFromQueue: function(index) {
+ var wasRunning = $scope.uploader.running;
+ $scope.uploadQueue[index].stop();
+ $scope.uploadQueue.splice(index, 1);
+ if (wasRunning)
+ $scope.go();
+ },
+ countDone: function() {
+ var done=0;
+ for (var i=0; i<$scope.uploadQueue.length; i++) {
+ if ($scope.uploadQueue[i].state == 'Done') {
+ ++done;
+ }
+ }
+ return done;
+ }
+ });
+ // TODO: watch uploadQueue, abort uploads if entries disappear
+
+ var keepProxy;
+
+ function SliceReader(_slice) {
+ var that = this;
+ $.extend(this, {
+ go: go
+ });
+ ////////////////////////////////
+ var _deferred;
+ var _reader;
+ function go() {
+ // Return a promise, which will be resolved with the
+ // requested slice data.
+ _deferred = $.Deferred();
+ _reader = new FileReader();
+ _reader.onload = resolve;
+ _reader.onerror = _deferred.reject;
+ _reader.onprogress = _deferred.notify;
+ _reader.readAsArrayBuffer(_slice.blob);
+ return _deferred.promise();
+ }
+ function resolve() {
+ if (that._reader.result.length != that._slice.size) {
+ // Sometimes we get an onload event even if the read
+ // did not return the desired number of bytes. We
+ // treat that as a fail.
+ _deferred.reject(
+ null, "Read error",
+ "Short read: wanted " + _slice.size +
+ ", received " + _reader.result.length);
+ return;
+ }
+ return _deferred.resolve(_reader.result);
+ }
+ }
+
+ function SliceUploader(_label, _data, _dataSize) {
+ $.extend(this, {
+ go: go,
+ stop: stop
+ });
+ ////////////////////////////////
+ var that = this;
+ var _deferred;
+ var _failCount = 0;
+ var _failMax = 3;
+ var _jqxhr;
+ function go() {
+ // Send data to the Keep proxy. Retry a few times on
+ // fail. Return a promise that will get resolved with
+ // resolve(locator) when the block is accepted by the
+ // proxy.
+ _deferred = $.Deferred();
+ goSend();
+ return _deferred.promise();
+ }
+ function stop() {
+ _failMax = 0;
+ _jqxhr.abort();
+ _deferred.reject({
+ textStatus: 'stopped',
+ err: 'interrupted at slice '+_label
+ });
+ }
+ function goSend() {
+ _jqxhr = $.ajax({
+ url: proxyUriBase(),
+ type: 'POST',
+ crossDomain: true,
+ headers: {
+ 'Authorization': 'OAuth2 '+arvadosApiToken,
+ 'Content-Type': 'application/octet-stream',
+ 'X-Keep-Desired-Replicas': '2'
+ },
+ xhr: function() {
+ // Make an xhr that reports upload progress
+ var xhr = $.ajaxSettings.xhr();
+ if (xhr.upload) {
+ xhr.upload.onprogress = onSendProgress;
+ }
+ return xhr;
+ },
+ processData: false,
+ data: _data
+ });
+ _jqxhr.then(onSendResolve, onSendReject);
+ }
+ function onSendProgress(xhrProgressEvent) {
+ _deferred.notify(xhrProgressEvent.loaded, _dataSize);
+ }
+ function onSendResolve(data, textStatus, jqxhr) {
+ _deferred.resolve(data, _dataSize);
+ }
+ function onSendReject(xhr, textStatus, err) {
+ if (++_failCount < _failMax) {
+ // TODO: nice to tell the user that retry is happening.
+ console.log('slice ' + _label + ': ' +
+ textStatus + ', retry ' + _failCount);
+ goSend();
+ } else {
+ _deferred.reject(
+ {xhr: xhr, textStatus: textStatus, err: err});
+ }
+ }
+ function proxyUriBase() {
+ return ((keepProxy.service_ssl_flag ? 'https' : 'http') +
+ '://' + keepProxy.service_host + ':' +
+ keepProxy.service_port + '/');
+ }
+ }
+
+ function FileUploader(file) {
+ $.extend(this, {
+ committed: false,
+ file: file,
+ locators: [],
+ progress: 0.0,
+ state: 'Queued', // Queued, Uploading, Paused, Done
+ statistics: null,
+ go: go,
+ stop: stop // User wants to stop.
+ });
+ ////////////////////////////////
+ var that = this;
+ var _currentUploader;
+ var _currentSlice;
+ var _deferred;
+ var _maxBlobSize = Math.pow(2,26);
+ var _bytesDone = 0;
+ var _queueTime = Date.now();
+ var _startTime;
+ var _startByte;
+ var _finishTime;
+ var _readPos = 0; // number of bytes confirmed uploaded
+ function go() {
+ if (_deferred)
+ _deferred.reject({textStatus: 'restarted'});
+ _deferred = $q.defer();
+ that.state = 'Uploading';
+ _startTime = Date.now();
+ _startByte = _readPos;
+ setProgress();
+ goSlice();
+ return _deferred.promise;
+ }
+ function stop() {
+ if (_deferred) {
+ that.state = 'Paused';
+ _deferred.reject({textStatus: 'stopped', err: 'interrupted'});
+ }
+ if (_currentUploader) {
+ _currentUploader.stop();
+ _currentUploader = null;
+ }
+ }
+ function goSlice() {
+ // Ensure this._deferred gets resolved or rejected --
+ // either right here, or when a new promise arranged right
+ // here is fulfilled.
+ _currentSlice = nextSlice();
+ if (!_currentSlice) {
+ that.state = 'Done';
+ setProgress(_readPos);
+ _currentUploader = null;
+ _deferred.resolve([that]);
+ return;
+ }
+ _currentUploader = new SliceUploader(
+ _readPos.toString(),
+ _currentSlice.blob,
+ _currentSlice.size);
+ _currentUploader.go().then(
+ onUploaderResolve,
+ onUploaderReject,
+ onUploaderProgress);
+ }
+ function onUploaderResolve(locator, dataSize) {
+ if (!locator || _currentSlice.size != dataSize) {
+ console.log("onUploaderResolve but locator=" + locator +
+ " and " + _currentSlice.size + " != " + dataSize);
+ return onUploaderReject({
+ textStatus: "error",
+ err: "Bad response from slice upload"
+ });
+ }
+ that.locators.push(locator);
+ _readPos += dataSize;
+ _currentUploader = null;
+ goSlice();
+ }
+ function onUploaderReject(reason) {
+ that.state = 'Paused';
+ setProgress(_readPos);
+ _currentUploader = null;
+ _deferred.reject(reason);
+ }
+ function onUploaderProgress(sliceDone, sliceSize) {
+ setProgress(_readPos + sliceDone);
+ }
+ function nextSlice() {
+ var size = Math.min(
+ _maxBlobSize,
+ that.file.size - _readPos);
+ setProgress(_readPos);
+ if (size == 0) {
+ return false;
+ }
+ var blob = that.file.slice(
+ _readPos, _readPos+size,
+ 'application/octet-stream; charset=x-user-defined');
+ return {blob: blob, size: size};
+ }
+ function setProgress(bytesDone) {
+ var kBps;
+ that.progress = Math.min(100, 100 * bytesDone / that.file.size)
+ if (bytesDone > _startByte) {
+ kBps = (bytesDone - _startByte) /
+ (Date.now() - _startTime);
+ that.statistics = (
+ '' + $filter('number')(bytesDone/1024, '0') + 'K ' +
+ 'at ~' + $filter('number')(kBps, '0') + 'K/s')
+ if (that.state == 'Paused') {
+ that.statistics += ', paused';
+ } else if (that.state == 'Uploading') {
+ that.statistics += ', ETA ' +
+ $filter('date')(
+ new Date(
+ Date.now() + (that.file.size - bytesDone) / kBps),
+ 'shortTime')
+ } else {
+ that.statistics += ', finished ' +
+ $filter('date')(Date.now(), 'shortTime');
+ _finishTime = Date.now();
+ }
+ } else {
+ that.statistics = that.state;
+ }
+ _deferred.notify();
+ }
+ }
+
+ function QueueUploader() {
+ $.extend(this, {
+ state: 'Idle',
+ stateReason: null,
+ statusSuccess: null,
+ go: go,
+ stop: stop
+ });
+ ////////////////////////////////
+ var that = this;
+ var _deferred;
+ function go() {
+ if (that.state == 'Running') return _deferred.promise;
+ _deferred = $.Deferred();
+ that.state = 'Running';
+ ArvadosClient.apiPromise(
+ 'keep_services', 'list',
+ {filters: [['service_type','=','proxy']]}).
+ then(doQueueWithProxy);
+ onQueueProgress();
+ return _deferred.promise();
+ }
+ function stop() {
+ for (var i=0; i<$scope.uploadQueue.length; i++)
+ $scope.uploadQueue[i].stop();
+ }
+ function doQueueWithProxy(data) {
+ keepProxy = data.items[0];
+ if (!keepProxy) {
+ that.state = 'Failed';
+ that.stateReason =
+ 'There seems to be no Keep proxy service available.';
+ _deferred.reject(null, 'error', that.stateReason);
+ return;
+ }
+ return doQueueWork();
+ }
+ function doQueueWork() {
+ var i;
+ that.state = 'Running';
+ that.stateReason = null;
+ // Push the done things to the bottom of the queue.
+ for (i=0; (i<$scope.uploadQueue.length &&
+ $scope.uploadQueue[i].state == 'Done'); i++);
+ if (i>0)
+ $scope.uploadQueue.push.apply($scope.uploadQueue, $scope.uploadQueue.splice(0, i));
+ // If anything is not-done, do it.
+ if ($scope.uploadQueue.length > 0 &&
+ $scope.uploadQueue[0].state != 'Done') {
+ return $scope.uploadQueue[0].go().
+ then(appendToCollection, null, onQueueProgress).
+ then(doQueueWork, onQueueReject);
+ }
+ // If everything is done, resolve the promise and clean up.
+ return onQueueResolve();
+ }
+ function onQueueReject(reason) {
+ that.state = 'Failed';
+ that.stateReason = (
+ (reason.textStatus || 'Error') +
+ (reason.xhr && reason.xhr.options
+ ? (' (from ' + reason.xhr.options.url + ')')
+ : '') +
+ ': ' +
+ (reason.err || ''));
+ if (reason.xhr && reason.xhr.responseText)
+ that.stateReason += ' -- ' + reason.xhr.responseText;
+ _deferred.reject(reason);
+ onQueueProgress();
+ }
+ function onQueueResolve() {
+ that.state = 'Idle';
+ that.stateReason = 'Done!';
+ _deferred.resolve();
+ onQueueProgress();
+ }
+ function onQueueProgress() {
+ // Ensure updates happen after FileUpload promise callbacks.
+ $timeout(function(){$scope.$apply();});
+ }
+ function appendToCollection(uploads) {
+ var deferred = $q.defer();
+ return ArvadosClient.apiPromise(
+ 'collections', 'get',
+ { uuid: $scope.uuid }).
+ then(function(collection) {
+ var manifestText = '';
+ var upload, i;
+ for (i=0; i<uploads.length; i++) {
+ upload = uploads[i];
+ filename = ArvadosClient.uniqueNameForManifest(
+ collection.manifest_text,
+ '.', upload.file.name);
+ collection.manifest_text += '. ' +
+ upload.locators.join(' ') +
+ ' 0:' + upload.file.size.toString() + ':' +
+ filename +
+ '\n';
+ }
+ return ArvadosClient.apiPromise(
+ 'collections', 'update',
+ { uuid: $scope.uuid,
+ collection:
+ { manifest_text:
+ collection.manifest_text }
+ }).
+ then(deferred.resolve);
+ }, onQueueReject).then(function() {
+ var i;
+ for(i=0; i<uploads.length; i++) {
+ uploads[i].committed = true;
+ }
+ });
+ return deferred.promise.then(doQueueWork);
+ }
+ }
+}
font-size: .8em;
color: #888;
}
+.lighten {
+ color: #888;
+}
.arvados-filename,
.arvados-uuid {
font-size: .8em;
elsif request.method.in? ['GET', 'HEAD']
render
else
- redirect_to params[:return_to] || @object
+ redirect_to (params[:return_to] ||
+ polymorphic_url(@object,
+ anchor: params[:redirect_to_anchor]))
end
}
f.js { render }
@new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
@object ||= model_class.new @new_resource_attrs, params["options"]
if @object.save
- respond_to do |f|
- f.json { render json: @object.attributes.merge(href: url_for(action: :show, id: @object)) }
- f.html {
- redirect_to @object
- }
- f.js { render }
- end
+ show
else
- self.render_error status: 422
+ render_error status: 422
end
end
RELATION_LIMIT = 5
def show_pane_list
- %w(Files Provenance_graph Used_by Advanced)
+ panes = %w(Files Upload Provenance_graph Used_by Advanced)
+ panes = panes - %w(Upload) unless (@object.editable? rescue false)
+ panes
end
def set_persistent
--- /dev/null
+<div class="arv-log-refresh-control"
+ data-load-throttle="86486400000" <%# 1001 nights %>
+ ></div>
+<div ng-cloak ng-controller="UploadToCollection" arv-uuid="<%= @object.uuid %>">
+ <div class="panel panel-primary">
+ <div class="panel-body">
+ <div class="row">
+ <div class="col-sm-4">
+ <input type="file" multiple ng-model="incoming" onchange="angular.element(this).scope().addFilesToQueue(this.files); $(this).val('');">
+ <div class="btn-group btn-group-sm" role="group" style="margin-top: 1.5em">
+ <button type="button" class="btn btn-default" ng-click="stop()" ng-disabled="uploader.state != 'Running'"><i class="fa fa-fw fa-pause"></i> Pause</button>
+ <button type="button" class="btn btn-primary" ng-click="go()" ng-disabled="uploader.state == 'Running' || uploadQueue.length == 0"><i class="fa fa-fw fa-play"></i> Start</button>
+ </div>
+ </div>
+ <div class="col-sm-8">
+ <div ng-show="uploader.state == 'Running'"
+ class="alert alert-info"
+ ><i class="fa fa-gear"></i>
+ Upload in progress.
+ <span ng-show="countDone() > 0">
+ {{countDone()}} file{{countDone()>1?'s':''}} finished.
+ </span>
+ </div>
+ <div ng-show="uploader.state == 'Idle' && uploader.stateReason"
+ class="alert alert-success"
+ ><i class="fa fa-flag-checkered"></i> {{uploader.stateReason}}
+ </div>
+ <div ng-show="uploader.state == 'Failed'"
+ class="alert alert-danger"
+ ><i class="fa fa-warning"></i> {{uploader.stateReason}}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div ng-repeat="upload in uploadQueue" class="row" ng-class="{lighten: upload.committed}">
+ <div class="col-sm-1">
+ <button class="btn btn-xs btn-default"
+ ng-show="!upload.committed"
+ ng-click="removeFileFromQueue($index)"
+ title="cancel"><i class="fa fa-fw fa-times"></i></button>
+ <span class="label label-success label-info"
+ ng-show="upload.committed">finished</span>
+ </div>
+ <div class="col-sm-4 nowrap" style="overflow-x:hidden;text-overflow:ellipsis">
+ <span title="{{upload.file.name}}">
+ {{upload.file.name}}
+ </span>
+ </div>
+ <div class="col-sm-1" style="text-align: right">
+ {{upload.file.size/1024 | number:0}}K
+ </div>
+ <div class="col-sm-2">
+ <div class="progress">
+ <span class="progress-bar" style="width: {{upload.progress}}%"></span>
+ </div>
+ </div>
+ <div class="col-sm-4" ng-class="{lighten: upload.state != 'Uploading'}">
+ {{upload.statistics}}
+ </div>
+ </div>
+</div>
<!DOCTYPE html>
-<html>
+<html ng-app="Workbench">
<head>
<meta charset="utf-8">
<title>
<%= csrf_meta_tags %>
<%= yield :head %>
<%= javascript_tag do %>
+ angular.module('Arvados').value('arvadosApiToken', '<%=Thread.current[:arvados_api_token]%>');
+ angular.module('Arvados').value('arvadosDiscoveryUri', '<%= Rails.configuration.arvados_v1_base.sub '/arvados/v1', '/discovery/v1/apis/arvados/v1/rest' %>');
<%= yield :js %>
<% end %>
<style>
<div class="panel-heading"><span class="panel-title">Active pipelines</span>
<span class="pull-right">
<%= link_to(
- choose_pipeline_templates_path(
- title: 'Choose a pipeline to run:',
- action_name: 'Next: choose inputs <i class="fa fa-fw fa-arrow-circle-right"></i>',
- action_href: pipeline_instances_path,
- action_method: 'post',
- action_data: {'selection_param' => 'pipeline_instance[pipeline_template_uuid]', 'pipeline_instance[owner_uuid]' => current_user.uuid, 'success' => 'redirect-to-created-object'}.to_json),
- { class: "btn btn-primary btn-xs", remote: true }) do %>
+ choose_pipeline_templates_path(
+ title: 'Choose a pipeline to run:',
+ action_name: 'Next: choose inputs <i class="fa fa-fw fa-arrow-circle-right"></i>',
+ action_href: pipeline_instances_path,
+ action_method: 'post',
+ action_data: {'selection_param' => 'pipeline_instance[pipeline_template_uuid]', 'pipeline_instance[owner_uuid]' => current_user.uuid, 'success' => 'redirect-to-created-object'}.to_json),
+ { class: "btn btn-primary btn-xs", remote: true }) do %>
<i class="fa fa-fw fa-gear"></i> Run a pipeline...
<% end %>
</span>
<% content_for :tab_line_buttons do %>
<% if @object.editable? %>
+ <div class="btn-group btn-group-sm">
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">Add data <span class="caret"></span></button>
+ <ul class="dropdown-menu" role="menu">
+ <li>
+ <%= link_to(
+ choose_collections_path(
+ title: 'Choose a collection to copy into this project:',
+ multiple: true,
+ action_name: 'Copy',
+ action_href: actions_path(id: @object.uuid),
+ action_method: 'post',
+ action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
+ { remote: true, title: "Copy a collection from another project into this one", data: {'event-after-select' => 'page-refresh', 'toggle' => 'dropdown'} }) do %>
+ <i class="fa fa-fw fa-clipboard"></i> ...from a different project
+ <% end %>
+ </li>
+ <li>
+ <%= link_to(collections_path(options: {ensure_unique_name: true}, collection: {manifest_text: "", name: "New collection", owner_uuid: @object.uuid}, redirect_to_anchor: 'Upload'), {
+ method: 'post',
+ title: "Upload files into a new collection",
+ data: {toggle: 'dropdown'}}) do %>
+ <i class="fa fa-fw fa-upload"></i> ...from your computer
+ <% end %>
+ </li>
+ </ul>
+ </div>
<%= link_to(
- choose_collections_path(
- title: 'Add data to project:',
- multiple: true,
- action_name: 'Add',
- action_href: actions_path(id: @object.uuid),
- action_method: 'post',
- action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
- { class: "btn btn-primary btn-sm", remote: true, title: "Add data to this project", data: {'event-after-select' => 'page-refresh'} }) do %>
- <i class="fa fa-fw fa-plus"></i> Add data...
- <% end %>
- <%= link_to(
- choose_pipeline_templates_path(
- title: 'Choose a pipeline to run:',
- action_name: 'Next: choose inputs <i class="fa fa-fw fa-arrow-circle-right"></i>',
- action_href: pipeline_instances_path,
- action_method: 'post',
- action_data: {'selection_param' => 'pipeline_instance[pipeline_template_uuid]', 'pipeline_instance[owner_uuid]' => @object.uuid, 'success' => 'redirect-to-created-object'}.to_json),
- { class: "btn btn-primary btn-sm", remote: true, title: "Run a pipeline in this project" }) do %>
+ choose_pipeline_templates_path(
+ title: 'Choose a pipeline to run:',
+ action_name: 'Next: choose inputs <i class="fa fa-fw fa-arrow-circle-right"></i>',
+ action_href: pipeline_instances_path,
+ action_method: 'post',
+ action_data: {'selection_param' => 'pipeline_instance[pipeline_template_uuid]', 'pipeline_instance[owner_uuid]' => @object.uuid, 'success' => 'redirect-to-created-object'}.to_json),
+ { class: "btn btn-primary btn-sm", remote: true, title: "Run a pipeline in this project" }) do %>
<i class="fa fa-fw fa-gear"></i> Run a pipeline...
<% end %>
<%= link_to projects_path({'project[owner_uuid]' => @object.uuid, 'options' => {'ensure_unique_name' => true}}), method: :post, title: "Add a subproject to this project", class: 'btn btn-sm btn-primary' do %>
<div class="modal-body">
<div> <%= link_to "Click here to learn about SSH keys in Arvados.",
- "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
- style: "font-weight: bold",
- target: "_blank" %>
+ "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
+ style: "font-weight: bold",
+ target: "_blank" %>
</div>
<div class="form-group">
<label for="public_key">Public Key</label>
<% if !@my_ssh_keys.any? %>
<p> You have not yet set up an SSH public key for use with Arvados. </p>
<p> <%= link_to "Click here",
- "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
- style: "font-weight: bold",
- target: "_blank" %> to learn about SSH keys in Arvados.
+ "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
+ style: "font-weight: bold",
+ target: "_blank" %> to learn about SSH keys in Arvados.
</p>
<p> When you have an SSH key you would like to use, add it using the <b>Add</b> button. </p>
<% else %>
find("#projects-menu").click
find('.dropdown-menu a,button', text: 'A Project').click
find('.btn', text: 'Add data').click
+ find('.dropdown-menu a,button', text: '...from a different project').click
within('.modal-dialog') do
wait_for_ajax
first('span', text: 'foo_tag').click
- find('.btn', text: 'Add').click
+ find('.btn', text: 'Copy').click
end
using_wait_time(Capybara.default_wait_time * 3) do
wait_for_ajax
find("#projects-menu").click
find('.dropdown-menu a,button', text: 'A Project').click
find('.btn', text: 'Add data').click
+ find('.dropdown-menu a,button', text: '...from a different project').click
within('.modal-dialog') do
wait_for_ajax
first('span', text: 'foo_tag').click
- find('.btn', text: 'Add').click
+ find('.btn', text: 'Copy').click
end
using_wait_time(Capybara.default_wait_time * 3) do
wait_for_ajax
+++ /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/
+++ /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/
+++ /dev/null
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-//= require jquery
-//= require jquery_ujs
-//= require_tree .
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /dev/null
-// -*- mode: javascript; js-indent-level: 4; indent-tabs-mode: nil; -*-
-// Place all the behaviors and hooks related to the matching controller here.
-// All this logic will automatically be available in application.js.
-
-var loaded_nodes_js;
-$(function(){
- if (loaded_nodes_js) return; loaded_nodes_js = true;
-
- $('[data-showhide-selector]').on('click', function(e){
- var x = $($(this).attr('data-showhide-selector'));
- if (x.css('display') == 'none')
- x.show();
- else
- x.hide();
- });
- $('[data-showhide-default]').hide();
-});
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/
+++ /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/