Merge "[NG] Enhance Create Image workflow with upload tracking"
This commit is contained in:
commit
f3f2ea1d62
@ -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) {
|
||||
|
@ -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({});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user