Merge "[NG] Enhance Create Image workflow with upload tracking"

This commit is contained in:
Jenkins 2016-08-10 16:29:48 +00:00 committed by Gerrit Code Review
commit f3f2ea1d62
8 changed files with 96 additions and 30 deletions

View File

@ -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) {

View File

@ -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({});
});
});

View File

@ -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'
};
}

View File

@ -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;

View File

@ -70,19 +70,23 @@
</div>
<div class="row form-group" ng-if="ctrl.isLocalFileUpload()">
<div class="col-xs-6 col-sm-6">
<div class="form-group required">
<div class="form-group file-upload required">
<label class="control-label" for="imageForm-image_url">
<translate>File</translate><span class="hz-icon-required fa fa-asterisk"></span>
</label>
<div class="input-group">
<div class="input-group" ng-hide="ctrl.uploadProgress > -1">
<span class="input-group-btn">
<button class="btn btn-primary" ng-model="image_file"
ngf-select="ctrl.prepareUpload(image_file)"
name="image_file" ng-required="true"
id="imageForm-image_file" translate>Browse...</button>
</span>
<button class="btn btn-primary" ng-model="image_file"
ngf-select="ctrl.prepareUpload(image_file)"
name="image_file" ng-required="true"
id="imageForm-image_file" translate>Browse...</button>
</span>
<input type="text" class="form-control" readonly ng-model="image_file.name">
</div>
<div ng-hide="ctrl.uploadProgress < 0" class="progress-text">
<progressbar value="ctrl.uploadProgress"></progressbar>
<span class="progress-bar-text">{$ ctrl.uploadProgress $}%</span>
</div>
<p class="help-block alert alert-danger"
ng-show="imageForm.image_file.$invalid && imageForm.image_file.$dirty">
<translate>A local file should be selected.</translate>

View File

@ -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);
}
/**

View File

@ -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]]);
});
});
});

View File

@ -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
<https://blueprints.launchpad.net/horizon/+spec/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.