diff --git a/openstack_dashboard/static/app/core/images/actions/create.action.service.js b/openstack_dashboard/static/app/core/images/actions/create.action.service.js index 454ff08197..b19d2e6ef3 100644 --- a/openstack_dashboard/static/app/core/images/actions/create.action.service.js +++ b/openstack_dashboard/static/app/core/images/actions/create.action.service.js @@ -115,7 +115,10 @@ } else { delete finalModel.image_url; } - return glance.createImage(finalModel).then(onCreateImage); + function onProgress(progress) { + scope.$broadcast(events.IMAGE_UPLOAD_PROGRESS, progress); + } + return glance.createImage(finalModel, onProgress).then(onCreateImage); } function onCreateImage(response) { diff --git a/openstack_dashboard/static/app/core/images/actions/create.action.service.spec.js b/openstack_dashboard/static/app/core/images/actions/create.action.service.spec.js index 3e5eb37fa0..97b8af7c44 100644 --- a/openstack_dashboard/static/app/core/images/actions/create.action.service.spec.js +++ b/openstack_dashboard/static/app/core/images/actions/create.action.service.spec.js @@ -115,8 +115,8 @@ modalArgs.submit(); $scope.$apply(); - expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test', - id: '2', prop1: '11', prop3: '3'}); + expect(glanceAPI.createImage.calls.argsFor(0)[0]).toEqual( + {name: 'Test', id: '2', prop1: '11', prop3: '3'}); }); it('does not pass location to create image if source_type is NOT url', function() { @@ -135,7 +135,7 @@ var modalArgs = wizardModalService.modal.calls.argsFor(0)[0]; modalArgs.submit(); - expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test', + expect(glanceAPI.createImage.calls.argsFor(0)[0]).toEqual({ name: 'Test', source_type: 'file-direct', data: {name: 'test_file'}}); }); @@ -155,7 +155,7 @@ var modalArgs = wizardModalService.modal.calls.argsFor(0)[0]; modalArgs.submit(); - expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test', + expect(glanceAPI.createImage.calls.argsFor(0)[0]).toEqual({ name: 'Test', source_type: 'url', image_url: 'http://somewhere'}); }); @@ -175,7 +175,7 @@ var modalArgs = wizardModalService.modal.calls.argsFor(0)[0]; modalArgs.submit(); - expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test', + expect(glanceAPI.createImage.calls.argsFor(0)[0]).toEqual({ name: 'Test', source_type: 'file-direct', data: {name: 'test_file'}}); }); @@ -195,7 +195,7 @@ var modalArgs = wizardModalService.modal.calls.argsFor(0)[0]; modalArgs.submit(); - expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test', + expect(glanceAPI.createImage.calls.argsFor(0)[0]).toEqual({ name: 'Test', source_type: 'url', image_url: 'http://somewhere'}); }); @@ -247,7 +247,7 @@ modalArgs.submit(); $scope.$apply(); - expect(glanceAPI.createImage).toHaveBeenCalledWith({}); + expect(glanceAPI.createImage.calls.argsFor(0)[0]).toEqual({}); }); }); diff --git a/openstack_dashboard/static/app/core/images/images.module.js b/openstack_dashboard/static/app/core/images/images.module.js index d6df4ade2c..50b1c18b83 100644 --- a/openstack_dashboard/static/app/core/images/images.module.js +++ b/openstack_dashboard/static/app/core/images/images.module.js @@ -280,7 +280,8 @@ return { VOLUME_CHANGED: 'horizon.app.core.images.VOLUME_CHANGED', IMAGE_CHANGED: 'horizon.app.core.images.IMAGE_CHANGED', - IMAGE_METADATA_CHANGED: 'horizon.app.core.images.IMAGE_METADATA_CHANGED' + IMAGE_METADATA_CHANGED: 'horizon.app.core.images.IMAGE_METADATA_CHANGED', + IMAGE_UPLOAD_PROGRESS: 'horizon.app.core.images.IMAGE_UPLOAD_PROGRESS' }; } diff --git a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js index 145a26b29d..7a0e0f3287 100644 --- a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js +++ b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js @@ -65,6 +65,8 @@ visibility: 'public' }; + ctrl.uploadProgress = -1; + ctrl.imageProtectedOptions = [ { label: gettext('Yes'), value: true }, { label: gettext('No'), value: false } @@ -93,9 +95,11 @@ init(); var imageChangedWatcher = $scope.$watchCollection('ctrl.image', watchImageCollection); + var watchUploadProgress = $scope.$on(events.IMAGE_UPLOAD_PROGRESS, watchImageUpload); $scope.$on('$destroy', function() { imageChangedWatcher(); + watchUploadProgress(); }); /////////////////////////// @@ -104,6 +108,10 @@ ctrl.image.data = file; } + function watchImageUpload(event, progress) { + ctrl.uploadProgress = progress; + } + function getConfiguredFormatsAndModes(response) { var settingsFormats = response.OPENSTACK_IMAGE_FORMATS; var uploadMode = response.HORIZON_IMAGES_UPLOAD_MODE; diff --git a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html index f6c1536d17..dc18aabf5f 100644 --- a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html +++ b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html @@ -70,19 +70,23 @@
-
+
-
+
- - + +
+
+ + {$ ctrl.uploadProgress $}% +

A local file should be selected. diff --git a/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js b/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js index 6b48bfb83d..99b6b21465 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js @@ -130,11 +130,14 @@ * True to import the image data to the image service otherwise * image data will be used in its current location * + * @param {function} onProgress + * A callback to pass upload progress back to caller. + * * Any parameters not listed above will be assigned as custom properites. * * @returns {Object} The result of the API call */ - function createImage(image) { + function createImage(image, onProgress) { var localFile; var method = image.source_type === 'file-legacy' ? 'post' : 'put'; if (image.source_type === 'file-direct' && 'data' in image) { @@ -154,19 +157,24 @@ external: true }).then( function success() { return response; }, - onError + onError, + notify ); } else { return response; } } + function notify(event) { + onProgress(Math.round(event.loaded / event.total * 100)); + } + function onError() { toastService.add('error', gettext('Unable to create the image.')); } return apiService[method]('/api/glance/images/', image) - .then(onImageQueued, onError); + .then(onImageQueued, onError, notify); } /** diff --git a/openstack_dashboard/static/app/core/openstack-service-api/glance.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/glance.service.spec.js index 24d30bbe1c..7d4a4f17e0 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/glance.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/glance.service.spec.js @@ -173,12 +173,14 @@ }); describe('createImage', function() { - var $q, $rootScope, imageQueuedPromise; + var $q, $rootScope, imageQueuedPromise, imageUploadPromise, onProgress; beforeEach(inject(function(_$q_, _$rootScope_) { $q = _$q_; $rootScope = _$rootScope_; imageQueuedPromise = $q.defer(); + imageUploadPromise = $q.defer(); + onProgress = jasmine.createSpy('onProgress'); spyOn(apiService, 'put').and.returnValue(imageQueuedPromise.promise); })); @@ -207,8 +209,8 @@ beforeEach(function() { apiService.put.and.returnValues( imageQueuedPromise.promise, - {then: angular.noop}); - service.createImage(imageData); + imageUploadPromise.promise); + service.createImage(imageData, onProgress); }); it('does not send the file itself during the first call', function() { @@ -232,7 +234,7 @@ expect(apiService.put.calls.count()).toBe(1); }); - it('external upload uses data from initial image creation', function() { + it('uses data from the initially created image', function() { imageQueuedPromise.resolve({data: queuedImage}); $rootScope.$apply(); @@ -249,6 +251,22 @@ ); }); + it('sends back upload progress', function() { + imageQueuedPromise.resolve({data: queuedImage}); + $rootScope.$apply(); + imageUploadPromise.notify({ + loaded: 1, + total: 2 + }); + imageUploadPromise.notify({ + loaded: 2, + total: 2 + }); + $rootScope.$apply(); + + expect(onProgress.calls.allArgs()).toEqual([[50], [100]]); + }); + }); describe('proxied (AKA legacy) upload of a local file', function() { @@ -256,15 +274,10 @@ var imageData = { name: 'test', source_type: 'file-legacy', diskFormat: 'iso', data: fakeFile }; - var queuedImage = { - 'name': imageData.name - }; beforeEach(function() { - var q = $q.defer(); - q.resolve({data: queuedImage}); - spyOn(apiService, 'post').and.returnValue(q.promise); - service.createImage(imageData); + spyOn(apiService, 'post').and.returnValue(imageUploadPromise.promise); + service.createImage(imageData, onProgress); }); it('emits one POST and not PUTs', function() { @@ -276,6 +289,19 @@ expect(apiService.post).toHaveBeenCalledWith('/api/glance/images/', imageData); }); + it('sends back upload progress', function() { + imageUploadPromise.notify({ + loaded: 1, + total: 2 + }); + imageUploadPromise.notify({ + loaded: 2, + total: 2 + }); + $rootScope.$apply(); + + expect(onProgress.calls.allArgs()).toEqual([[50], [100]]); + }); }); }); diff --git a/releasenotes/notes/bp-horizon-glance-large-image-upload-c987dc86bab38761.yaml b/releasenotes/notes/bp-horizon-glance-large-image-upload-c987dc86bab38761.yaml new file mode 100644 index 0000000000..97d26b33c8 --- /dev/null +++ b/releasenotes/notes/bp-horizon-glance-large-image-upload-c987dc86bab38761.yaml @@ -0,0 +1,16 @@ +--- +features: + - Create from a local file feature is added to the Angular + Create Image workflow. It works either in a 'legacy' mode + which proxies an image upload through Django, or in a new + 'direct' mode, which in turn implements + [`blueprint horizon-glance-large-image-upload + `_]. + To use the direct mode HORIZON_IMAGES_UPLOAD_MODE setting + should be changed to 'direct' value along with changing + glance-api.conf cors.allowed_origin parameter to the URL + from which Horizon is served. +deprecations: + - HORIZON_IMAGES_ALLOW_UPLOAD setting is deprecated and + should be gradually replaced with + HORIZON_IMAGES_UPLOAD_MODE setting.