[ARVADOS] updated: 12b9d77ecca3043b65015860359ce447b68fccfb

git at public.curoverse.com git at public.curoverse.com
Wed Nov 26 01:18:43 EST 2014


Summary of changes:
 doc/_includes/_tutorial_expectations.liquid        |   4 +-
 doc/css/bootstrap.css                              |   6 +-
 doc/install/install-keepproxy.html.textile.liquid  |   9 +-
 doc/install/install-keepstore.html.textile.liquid  |   9 +-
 .../install-shell-server.html.textile.liquid       |   2 +-
 doc/sdk/cli/install.html.textile.liquid            |   4 +-
 doc/sdk/cli/subcommands.html.textile.liquid        | 148 +++++++++++++++++++++
 doc/sdk/python/sdk-python.html.textile.liquid      |  21 ++-
 .../check-environment.html.textile.liquid          |   2 +-
 .../tutorials/tutorial-keep.html.textile.liquid    |  54 ++++----
 sdk/cli/Gemfile.lock                               |  40 +++---
 .../api/app/controllers/application_controller.rb  |  35 +++++
 .../arvados/v1/keep_disks_controller.rb            |  14 +-
 .../app/controllers/arvados/v1/nodes_controller.rb |   2 +-
 .../controllers/arvados/v1/schema_controller.rb    |   8 +-
 .../app/controllers/user_sessions_controller.rb    |   4 +
 services/api/config/routes.rb                      |   9 +-
 services/api/db/structure.sql                      |   2 +-
 .../test/functional/application_controller_test.rb |  44 ++++++
 .../arvados/v1/job_reuse_controller_test.rb        |  38 +++---
 .../arvados/v1/keep_disks_controller_test.rb       |  31 +++--
 services/api/test/integration/cross_origin_test.rb |  76 +++++++++++
 22 files changed, 445 insertions(+), 117 deletions(-)
 create mode 100644 services/api/test/integration/cross_origin_test.rb

  discards  1df5dac7a399d6e8d740ef861229ddab8b698442 (commit)
  discards  bb32ded4360a7ff35205bb620ff448c342e0883a (commit)
  discards  c836dc7a116bc3385333af9fa4d2297e09591bd6 (commit)
  discards  c5ea28a77f5ba29e8d953f5b14147ab0addd5d84 (commit)
  discards  c9d3d15d17906aa4d65a0bb044596621be6516a7 (commit)
  discards  78191f9893da5bbfc33f46c2794ca6d92a61be17 (commit)
  discards  23f6cfa6ed5686a79f7945cf8a2cb9ef46cf7d46 (commit)
  discards  90b59fc16bae23485dbdcb8af3009c2efc03e6d6 (commit)
  discards  d95550d1827701788a59b2c834caddf4e3e1729d (commit)
  discards  fc8881447e9a0072206a08feedf9a9a40b770a3a (commit)
  discards  0d01ca86e6c73cb31efed555cbc645a8afab32eb (commit)
  discards  cd0ecbdddd9eeac9063d204de80dece4c75c01bb (commit)
  discards  60743d6764aa690d97509aa5bcaa4731f36b67a9 (commit)
  discards  cee4332f5921946203d20c0da6bbde13e3d96622 (commit)
  discards  7d0d7b0c3c66242c6ccf74613277092a3ff06ebc (commit)
  discards  cfc9ed031a0a689d92907ae201a1a18385f04ea4 (commit)
  discards  d6a28a4d11255751c0c954c0cd1d9c0e79069e6c (commit)
  discards  d2e9fba1946ead6c059d3df3cf34139563f2db60 (commit)
  discards  a75145983529310528a6cf0a54d78f85e9101eb6 (commit)
  discards  bfe5b9d98f73532231f8a2d4c0f8906e28d536f0 (commit)
  discards  1fc13b7710c858b88cdd15957202d3b43000f33f (commit)
  discards  9d5164a06a3dbf2732a149f4e1b9945b942fb30f (commit)
  discards  b982b8ac8c61b862067ced40e001714d6dedf800 (commit)
  discards  3df51093d693e89e14e35d3c5f73e74978b7e20e (commit)
  discards  af04cad1fbc8b90fca94610b312b1f710a0a94b1 (commit)
  discards  a5a99ae63eaae6b10b7718f6e685eecd99137495 (commit)
  discards  106746eaa37489d6f4277ea49f04c897dcb8bd23 (commit)
       via  12b9d77ecca3043b65015860359ce447b68fccfb (commit)
       via  f12f617fb736ddd48e4f2b9f57a6983ff8fe1ed3 (commit)
       via  556503e1f98b8e262fcc1227ac4afdc78a2c05ca (commit)
       via  03d9a63f77f5f1672186547870ef55c42e3a16a2 (commit)
       via  1f91e02c279bc1e7c47d88da43b72c6882b0ad77 (commit)
       via  4faccebcdd7872eca76b183951a2850518d35916 (commit)
       via  500fb090fb7c295000c485ca7e2214ad448c40dc (commit)
       via  3ae43002e9762e60844723edc5096c7282fb978d (commit)
       via  afef51be31f166fda47481e558881de09e97a983 (commit)
       via  a25c9cb6721e61afe433a238b2e2c580adf97f31 (commit)
       via  84efd065eba386e98e07cdd232dc172e169af451 (commit)
       via  7b8db198ad4cf91e605f099f78f5c4a1bef152ff (commit)
       via  257ecfece0f6941011c85e735459d86b9f850d25 (commit)
       via  333402104e6b7a163bf3f8483a928dbe571b5c2c (commit)
       via  60cf64002cee6af43fe8b6a122c104a12c1fd7bf (commit)
       via  1a4282c6d30b209a882c255e0d5777851ff6f034 (commit)
       via  f3d43ab311114a7c25b7ecc47f63affdc7197efb (commit)
       via  a8c9797de0fac6cc28d04daeade83d5e0c558076 (commit)
       via  aa1069044b7dbbba7d5fc302adfe7fc50efe9472 (commit)
       via  399a90e3372ce5255231179777190770934e585a (commit)
       via  e828f379a48ac0c60794f6e4c2969158afcb4f48 (commit)
       via  ca493dcca8463dc5976b31de0b0dfed3c4d26d9b (commit)
       via  7a503f8bceb9969d41116ed04f1b9edf53c169c2 (commit)
       via  34609757e88667be13d2e8294a2cfbd4c5cccfac (commit)
       via  efbf7dad04f239fd83ed477a3b9a1cf5b5b979aa (commit)
       via  a34aaf6d03786c865d327cf16537ddf3ff221b3a (commit)
       via  b9d52229719c49e13aa8dfaae69da88a23fbfe44 (commit)
       via  4c7615263f17507405895ceb906f7ecae0658d1d (commit)
       via  ac4cdfc2577b9d25ccbc9ac5d8f0333a81102367 (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (1df5dac7a399d6e8d740ef861229ddab8b698442)
            \
             N -- N -- N (12b9d77ecca3043b65015860359ce447b68fccfb)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 12b9d77ecca3043b65015860359ce447b68fccfb
Merge: 556503e f12f617
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Nov 26 00:57:06 2014 -0500

    3781: Merge branch '3781-browser-friendly-servers' into 3781-browser-upload


commit 556503e1f98b8e262fcc1227ac4afdc78a2c05ca
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Nov 22 14:40:34 2014 -0500

    3781: Add browser->api/keepproxy angular app as Upload tab on collections#show

diff --git a/apps/workbench/Gemfile b/apps/workbench/Gemfile
index 5ab6eac..365cefc 100644
--- a/apps/workbench/Gemfile
+++ b/apps/workbench/Gemfile
@@ -56,6 +56,8 @@ gem 'bootstrap-sass', '~> 3.1.0'
 gem 'bootstrap-x-editable-rails'
 gem 'bootstrap-tab-history-rails'
 
+gem 'angularjs-rails'
+
 gem 'less'
 gem 'less-rails'
 gem 'wiselinks'
diff --git a/apps/workbench/Gemfile.lock b/apps/workbench/Gemfile.lock
index 8b9ea94..8c18847 100644
--- a/apps/workbench/Gemfile.lock
+++ b/apps/workbench/Gemfile.lock
@@ -38,6 +38,7 @@ GEM
       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)
@@ -242,6 +243,7 @@ PLATFORMS
 DEPENDENCIES
   RedCloth
   andand
+  angularjs-rails
   arvados (>= 0.1.20141114230720)
   bootstrap-sass (~> 3.1.0)
   bootstrap-tab-history-rails
diff --git a/apps/workbench/app/assets/javascripts/angular_shim.js b/apps/workbench/app/assets/javascripts/angular_shim.js
new file mode 100644
index 0000000..a480eaf
--- /dev/null
+++ b/apps/workbench/app/assets/javascripts/angular_shim.js
@@ -0,0 +1,12 @@
+// 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);
+        });
+    }
+});
diff --git a/apps/workbench/app/assets/javascripts/application.js b/apps/workbench/app/assets/javascripts/application.js
index 1990b8b..6b98fd9 100644
--- a/apps/workbench/app/assets/javascripts/application.js
+++ b/apps/workbench/app/assets/javascripts/application.js
@@ -23,15 +23,10 @@
 //= require bootstrap3-editable/bootstrap-editable
 //= require bootstrap-tab-history
 //= require wiselinks
+//= require angular
 //= 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(){
diff --git a/apps/workbench/app/assets/javascripts/arvados_client.js b/apps/workbench/app/assets/javascripts/arvados_client.js
new file mode 100644
index 0000000..584928f
--- /dev/null
+++ b/apps/workbench/app/assets/javascripts/arvados_client.js
@@ -0,0 +1,94 @@
+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;
+    }
+}
diff --git a/apps/workbench/app/assets/javascripts/tab_panes.js b/apps/workbench/app/assets/javascripts/tab_panes.js
index 07e46fe..f603440 100644
--- a/apps/workbench/app/assets/javascripts/tab_panes.js
+++ b/apps/workbench/app/assets/javascripts/tab_panes.js
@@ -124,7 +124,7 @@ $(document).on('arv:pane:reload', '[data-pane-content-url]', function(e) {
             $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');
diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js
new file mode 100644
index 0000000..267543c
--- /dev/null
+++ b/apps/workbench/app/assets/javascripts/upload_to_collection.js
@@ -0,0 +1,419 @@
+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);
+        }
+    }
+}
diff --git a/apps/workbench/app/assets/stylesheets/application.css.scss b/apps/workbench/app/assets/stylesheets/application.css.scss
index 7dbeac9..8b5580f 100644
--- a/apps/workbench/app/assets/stylesheets/application.css.scss
+++ b/apps/workbench/app/assets/stylesheets/application.css.scss
@@ -47,6 +47,9 @@ table.table-justforlayout {
     font-size: .8em;
     color: #888;
 }
+.lighten {
+    color: #888;
+}
 .arvados-filename,
 .arvados-uuid {
     font-size: .8em;
diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb
index 3270cfb..6fea625 100644
--- a/apps/workbench/app/controllers/application_controller.rb
+++ b/apps/workbench/app/controllers/application_controller.rb
@@ -256,7 +256,9 @@ class ApplicationController < ActionController::Base
         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 }
@@ -321,15 +323,9 @@ class ApplicationController < ActionController::Base
     @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
 
diff --git a/apps/workbench/app/controllers/collections_controller.rb b/apps/workbench/app/controllers/collections_controller.rb
index 39f637e..eacf8b1 100644
--- a/apps/workbench/app/controllers/collections_controller.rb
+++ b/apps/workbench/app/controllers/collections_controller.rb
@@ -14,7 +14,9 @@ class CollectionsController < ApplicationController
   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
diff --git a/apps/workbench/app/views/collections/_show_upload.html.erb b/apps/workbench/app/views/collections/_show_upload.html.erb
new file mode 100644
index 0000000..f85f628
--- /dev/null
+++ b/apps/workbench/app/views/collections/_show_upload.html.erb
@@ -0,0 +1,62 @@
+<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>
diff --git a/apps/workbench/app/views/layouts/application.html.erb b/apps/workbench/app/views/layouts/application.html.erb
index 324714e..cdc47c1 100644
--- a/apps/workbench/app/views/layouts/application.html.erb
+++ b/apps/workbench/app/views/layouts/application.html.erb
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html>
+<html ng-app="Workbench">
 <head>
   <meta charset="utf-8">
   <title>
@@ -23,6 +23,8 @@
   <%= 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>
diff --git a/apps/workbench/app/views/projects/show.html.erb b/apps/workbench/app/views/projects/show.html.erb
index 0429f33..58d8f4e 100644
--- a/apps/workbench/app/views/projects/show.html.erb
+++ b/apps/workbench/app/views/projects/show.html.erb
@@ -6,17 +6,32 @@
 
 <% content_for :tab_line_buttons do %>
   <% if @object.editable? %>
-    <%= link_to(
-	  choose_collections_path(
-	    title: 'Add data to project:',
-	    multiple: true,
-	    action_name: 'Add',
-	    action_href: actions_path(id: @object.uuid),
-	    action_method: 'post',
-	    action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
-	  { class: "btn btn-primary btn-sm", remote: true, method: 'get', title: "Add data to this project", data: {'event-after-select' => 'page-refresh'} }) do %>
-      <i class="fa fa-fw fa-plus"></i> Add data...
-    <% end %>
+    <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, method: 'get', 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_pipeline_templates_path(
 	    title: 'Choose a pipeline to run:',
diff --git a/apps/workbench/test/integration/pipeline_instances_test.rb b/apps/workbench/test/integration/pipeline_instances_test.rb
index 7e3696c..1669376 100644
--- a/apps/workbench/test/integration/pipeline_instances_test.rb
+++ b/apps/workbench/test/integration/pipeline_instances_test.rb
@@ -34,10 +34,11 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
     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
@@ -124,10 +125,11 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
     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

commit 03d9a63f77f5f1672186547870ef55c42e3a16a2
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Nov 22 04:44:00 2014 -0500

    3781: Remove js cruft from api server.

diff --git a/services/api/app/assets/javascripts/api_client_authorizations.js.coffee b/services/api/app/assets/javascripts/api_client_authorizations.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/api_clients.js.coffee b/services/api/app/assets/javascripts/api_clients.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/api_clients.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/services/api/app/assets/javascripts/application.js b/services/api/app/assets/javascripts/application.js
deleted file mode 100644
index 37c7bfc..0000000
--- a/services/api/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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 .
diff --git a/services/api/app/assets/javascripts/authorized_keys.js.coffee b/services/api/app/assets/javascripts/authorized_keys.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/collections.js.coffee b/services/api/app/assets/javascripts/collections.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/collections.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/services/api/app/assets/javascripts/commit_ancestors.js.coffee b/services/api/app/assets/javascripts/commit_ancestors.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/commit_ancestors.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/services/api/app/assets/javascripts/commits.js.coffee b/services/api/app/assets/javascripts/commits.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/commits.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/services/api/app/assets/javascripts/groups.js.coffee b/services/api/app/assets/javascripts/groups.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/humans.js.coffee b/services/api/app/assets/javascripts/humans.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/job_tasks.js.coffee b/services/api/app/assets/javascripts/job_tasks.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/jobs.js.coffee b/services/api/app/assets/javascripts/jobs.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/keep_disks.js.coffee b/services/api/app/assets/javascripts/keep_disks.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/keep_disks.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/services/api/app/assets/javascripts/links.js.coffee b/services/api/app/assets/javascripts/links.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/logs.js.coffee b/services/api/app/assets/javascripts/logs.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/nodes.js b/services/api/app/assets/javascripts/nodes.js
deleted file mode 100644
index a734426..0000000
--- a/services/api/app/assets/javascripts/nodes.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// -*- 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();
-});
diff --git a/services/api/app/assets/javascripts/nodes.js.coffee b/services/api/app/assets/javascripts/nodes.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/pipeline_instances.js.coffee b/services/api/app/assets/javascripts/pipeline_instances.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/pipeline_instances.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/services/api/app/assets/javascripts/pipeline_templates.js.coffee b/services/api/app/assets/javascripts/pipeline_templates.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/repositories.js.coffee b/services/api/app/assets/javascripts/repositories.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/specimens.js.coffee b/services/api/app/assets/javascripts/specimens.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/traits.js.coffee b/services/api/app/assets/javascripts/traits.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/services/api/app/assets/javascripts/virtual_machines.js.coffee b/services/api/app/assets/javascripts/virtual_machines.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/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/

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list