Adding Edit Image Action to angular images panel
Adds the ability to edit an image from the table. To test set DISABLED = False in _1051_project_ng_images_panel.py Also, ensure in your local_settings.py that you set: REST_API_REQUIRED_SETTINGS to include 'OPENSTACK_IMAGE_FORMATS' Co-Authored-By: Rajat Vig<rajatv@thoughtworks.com> Co-Authored-By: Nathan Zeplowitz<nzeplowi@thoughtworks.com> Co-Authored-By: Matt Borland <matt.borland@hpe.com> Co-Authored-By: Kyle Olivo<keolivo@thoughtworks.com> Change-Id: I046eca486ebc0d2d980ee706105e202bf9b15ba8 Partially-Implements: blueprint angularize-images-table
This commit is contained in:
parent
65eb9e9b8c
commit
5b06cda113
@ -297,10 +297,13 @@ def create_image_metadata(data):
|
||||
'container_format': data.get('container_format'),
|
||||
'properties': {}}
|
||||
|
||||
# 'description' and 'architecture' will be directly mapped
|
||||
# 'architecture' will be directly mapped
|
||||
# into the .properties by the handle_unknown_properties function.
|
||||
# 'kernel' and 'ramdisk' need to get specifically mapped for backwards
|
||||
# compatibility.
|
||||
props = data.get('properties')
|
||||
if props and props.get('description'):
|
||||
meta['properties']['description'] = props.get('description')
|
||||
if data.get('kernel'):
|
||||
meta['properties']['kernel_id'] = data.get('kernel')
|
||||
if data.get('ramdisk'):
|
||||
@ -320,9 +323,13 @@ def handle_unknown_properties(data, meta):
|
||||
# existing horizon api wrapper, we need this function. This way, the
|
||||
# client REST mirrors the Glance API.
|
||||
known_props = ['visibility', 'protected', 'disk_format',
|
||||
'container_format', 'min_disk', 'min_ram',
|
||||
'name', 'properties', 'kernel', 'ramdisk',
|
||||
'tags', 'import_data', 'source', 'image_url', 'source_type']
|
||||
'container_format', 'min_disk', 'min_ram', 'name',
|
||||
'properties', 'kernel', 'ramdisk',
|
||||
'tags', 'import_data', 'source',
|
||||
'image_url', 'source_type',
|
||||
'checksum', 'created_at', 'deleted', 'is_copying',
|
||||
'deleted_at', 'is_public', 'virtual_size',
|
||||
'status', 'size', 'owner', 'id', 'updated_at']
|
||||
other_props = {k: v for (k, v) in data.items() if k not in known_props}
|
||||
meta['properties'].update(other_props)
|
||||
|
||||
|
@ -720,7 +720,8 @@ SECURITY_GROUP_RULES = {
|
||||
# You should not add settings to this list for out of tree extensions.
|
||||
# See: https://wiki.openstack.org/wiki/Horizon/RESTAPI
|
||||
REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES',
|
||||
'LAUNCH_INSTANCE_DEFAULTS']
|
||||
'LAUNCH_INSTANCE_DEFAULTS',
|
||||
'OPENSTACK_IMAGE_FORMATS']
|
||||
|
||||
# Additional settings can be made available to the client side for
|
||||
# extensibility by specifying them in REST_API_ADDITIONAL_SETTINGS
|
||||
|
@ -305,6 +305,11 @@ if os.path.exists(LOCAL_SETTINGS_DIR_PATH):
|
||||
logging.exception(
|
||||
"Can not exec settings snippet %s" % filename)
|
||||
|
||||
# The purpose of OPENSTACK_IMAGE_FORMATS is to provide a simple object
|
||||
# that does not contain the lazy-loaded translations, so the list can
|
||||
# be sent as JSON to the client-side (Angular).
|
||||
OPENSTACK_IMAGE_FORMATS = [fmt for (fmt, name)
|
||||
in OPENSTACK_IMAGE_BACKEND['image_formats']]
|
||||
|
||||
if not WEBROOT.endswith('/'):
|
||||
WEBROOT += '/'
|
||||
|
@ -29,6 +29,7 @@
|
||||
|
||||
registerImageActions.$inject = [
|
||||
'horizon.framework.conf.resource-type-registry.service',
|
||||
'horizon.app.core.images.actions.edit.service',
|
||||
'horizon.app.core.images.actions.create-volume.service',
|
||||
'horizon.app.core.images.actions.delete-image.service',
|
||||
'horizon.app.core.images.actions.launch-instance.service',
|
||||
@ -38,6 +39,7 @@
|
||||
|
||||
function registerImageActions(
|
||||
registry,
|
||||
editService,
|
||||
createVolumeService,
|
||||
deleteImageService,
|
||||
launchInstanceService,
|
||||
@ -60,6 +62,13 @@
|
||||
text: gettext('Create Volume')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'editAction',
|
||||
service: editService,
|
||||
template: {
|
||||
text: gettext('Edit Image')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'updateMetadataService',
|
||||
service: updateMetadataService,
|
||||
|
@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.factory('horizon.app.core.images.actions.edit.service', editService);
|
||||
|
||||
editService.$inject = [
|
||||
'$q',
|
||||
'horizon.app.core.images.events',
|
||||
'horizon.app.core.images.actions.editWorkflow',
|
||||
'horizon.app.core.metadata.service',
|
||||
'horizon.app.core.openstack-service-api.glance',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.app.core.openstack-service-api.userSession',
|
||||
'horizon.framework.util.q.extensions',
|
||||
'horizon.framework.widgets.modal.wizard-modal.service',
|
||||
'horizon.framework.widgets.toast.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.app.core.images.actions.editService
|
||||
* @Description A service to open the user wizard.
|
||||
*/
|
||||
function editService(
|
||||
$q,
|
||||
events,
|
||||
editWorkflow,
|
||||
metadataService,
|
||||
glance,
|
||||
policy,
|
||||
userSessionService,
|
||||
$qExtensions,
|
||||
wizardModalService,
|
||||
toast
|
||||
) {
|
||||
var message = {
|
||||
success: gettext('Image %s was successfully updated.'),
|
||||
successMetadata: gettext('Image metadata %s was successfully updated.')
|
||||
};
|
||||
var modifyImagePolicyCheck, scope;
|
||||
|
||||
var model = {
|
||||
image: {},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
var service = {
|
||||
initScope: initScope,
|
||||
allowed: allowed,
|
||||
perform: perform
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
// include this function in your service
|
||||
// if you plan to emit events to the parent controller
|
||||
function initScope($scope) {
|
||||
var watchImageChange = $scope.$on(events.IMAGE_CHANGED, onImageChange);
|
||||
var watchMetadataChange = $scope.$on(events.IMAGE_METADATA_CHANGED, onMetadataChange);
|
||||
|
||||
scope = $scope;
|
||||
modifyImagePolicyCheck = policy.ifAllowed({rules: [['image', 'modify_image']]});
|
||||
|
||||
$scope.$on('$destroy', destroy);
|
||||
|
||||
function destroy() {
|
||||
watchImageChange();
|
||||
watchMetadataChange();
|
||||
}
|
||||
}
|
||||
|
||||
function onImageChange(e, image) {
|
||||
model.image = image;
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function onMetadataChange(e, metadata) {
|
||||
model.metadata = metadata;
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function allowed(image) {
|
||||
return $q.all([
|
||||
modifyImagePolicyCheck,
|
||||
userSessionService.isCurrentProject(image.owner),
|
||||
isActive(image)
|
||||
]);
|
||||
}
|
||||
|
||||
function perform(image) {
|
||||
var deferred = glance.getImage(image.id);
|
||||
deferred.then(onLoad);
|
||||
scope.imagePromise = deferred;
|
||||
|
||||
function onLoad(response) {
|
||||
var localImage = response.data;
|
||||
model.image = localImage;
|
||||
}
|
||||
|
||||
return wizardModalService.modal({
|
||||
scope: scope,
|
||||
workflow: editWorkflow,
|
||||
submit: submit
|
||||
}).result;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
return saveMetadata().then(onSaveMetadata, onFailMetadata);
|
||||
}
|
||||
|
||||
function onFailMetadata() {
|
||||
glance.updateImage(model.image).then(onUpdateImageSuccess, onUpdateImageFail);
|
||||
}
|
||||
|
||||
function onSaveMetadata() {
|
||||
toast.add('success', interpolate(message.successMetadata, [model.image.name]));
|
||||
glance.updateImage(model.image).then(onUpdateImageSuccess, onUpdateImageFail);
|
||||
}
|
||||
|
||||
function onUpdateImageSuccess() {
|
||||
toast.add('success', interpolate(message.success, [model.image.name]));
|
||||
scope.$emit(events.UPDATE_SUCCESS, model.image);
|
||||
return {
|
||||
// This will be filled out with useful information as it is
|
||||
// decided upon.
|
||||
};
|
||||
}
|
||||
|
||||
function onUpdateImageFail() {
|
||||
scope.$emit(events.UPDATE_SUCCESS, model.image);
|
||||
}
|
||||
|
||||
function saveMetadata() {
|
||||
var imageId = model.image.id;
|
||||
var deferred = $q.defer();
|
||||
|
||||
metadataService.getMetadata('image', imageId).then(onMetadataGet);
|
||||
|
||||
function onMetadataGet(response) {
|
||||
var removed = angular.copy(response.data);
|
||||
angular.forEach(model.metadata, function(value, key) {
|
||||
delete removed[key];
|
||||
});
|
||||
|
||||
deferred.resolve(
|
||||
metadataService.editMetadata('image', imageId, model.metadata, Object.keys(removed))
|
||||
);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function isActive(image) {
|
||||
return $qExtensions.booleanAsPromise(image.status === 'active');
|
||||
}
|
||||
|
||||
} // end of editService
|
||||
})(); // end of IIFE
|
@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use self file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.images.actions.edit.service', function() {
|
||||
var existingMetadata = {p1: '1', p2: '2'};
|
||||
|
||||
var metadataService = {
|
||||
getMetadata: function() {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback({
|
||||
data: existingMetadata
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
editMetadata: function() {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var wizardModalService = {
|
||||
modal: function () {
|
||||
return { result: {} };
|
||||
}
|
||||
};
|
||||
|
||||
var glanceAPI = {
|
||||
updateImage: function(image) {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback({data: image});
|
||||
}
|
||||
};
|
||||
},
|
||||
getImage: function() {
|
||||
var imageLoad = $q.defer();
|
||||
imageLoad.resolve({data: testImage});
|
||||
return imageLoad.promise;
|
||||
}
|
||||
};
|
||||
|
||||
var policyAPI = {
|
||||
ifAllowed: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({allowed: true});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var userSession = {
|
||||
isCurrentProject: function() {
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
var service, events, $scope, $q, toast, deferred, testImage, $timeout;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.app.core'));
|
||||
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('horizon.app.core.openstack-service-api.glance', glanceAPI);
|
||||
$provide.value('horizon.app.core.openstack-service-api.userSession', userSession);
|
||||
$provide.value('horizon.app.core.openstack-service-api.policy', policyAPI);
|
||||
$provide.value('horizon.app.core.metadata.service', metadataService);
|
||||
$provide.value('horizon.framework.widgets.modal.wizard-modal.service', wizardModalService);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector, _$rootScope_, _$q_, _$timeout_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
$q = _$q_;
|
||||
service = $injector.get('horizon.app.core.images.actions.edit.service');
|
||||
events = $injector.get('horizon.app.core.images.events');
|
||||
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||
service.initScope($scope);
|
||||
deferred = $q.defer();
|
||||
$timeout = _$timeout_;
|
||||
}));
|
||||
|
||||
describe('perform', function() {
|
||||
it('should open the modal with the correct parameters', function() {
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
testImage = {id: '12'};
|
||||
service.initScope($scope);
|
||||
service.perform(testImage);
|
||||
$timeout.flush();
|
||||
|
||||
expect(wizardModalService.modal).toHaveBeenCalled();
|
||||
expect($scope.imagePromise).toBeDefined();
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
expect(modalArgs.scope).toEqual($scope);
|
||||
expect(modalArgs.workflow).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update image in glance, update metadata and raise event', function() {
|
||||
testImage = { name: 'Test', id: '2' };
|
||||
var newImage = { name: 'Test2', id: '2' };
|
||||
var newMetadata = {p1: '11', p3: '3'};
|
||||
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(glanceAPI, 'updateImage').and.callThrough();
|
||||
spyOn(metadataService, 'editMetadata').and.callThrough();
|
||||
spyOn(toast, 'add').and.callThrough();
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform(testImage);
|
||||
$timeout.flush();
|
||||
|
||||
$scope.$emit(events.IMAGE_CHANGED, newImage);
|
||||
$scope.$emit(events.IMAGE_METADATA_CHANGED, newMetadata);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
$scope.$apply();
|
||||
|
||||
expect(glanceAPI.updateImage).toHaveBeenCalledWith(newImage);
|
||||
expect(metadataService.editMetadata)
|
||||
.toHaveBeenCalledWith('image', '2', newMetadata, ['p2']);
|
||||
expect(toast.add)
|
||||
.toHaveBeenCalledWith('success', 'Image Test2 was successfully updated.');
|
||||
expect(toast.add.calls.count()).toBe(2);
|
||||
expect($scope.$emit)
|
||||
.toHaveBeenCalledWith('horizon.app.core.images.UPDATE_SUCCESS', newImage);
|
||||
});
|
||||
|
||||
it('should raise event even if update meta data fails', function() {
|
||||
var image = { name: 'Test', id: '2' };
|
||||
var newImage = { name: 'Test2', id: '2' };
|
||||
var newMetadata = {prop1: '11', prop3: '3'};
|
||||
|
||||
var failedPromise = function() {
|
||||
return {
|
||||
then: function(callback, errorCallback) {
|
||||
errorCallback();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
spyOn(glanceAPI, 'updateImage').and.callThrough();
|
||||
spyOn(metadataService, 'editMetadata').and.callFake(failedPromise);
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(toast, 'add').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform(image);
|
||||
$scope.$apply();
|
||||
|
||||
$scope.$emit(events.IMAGE_CHANGED, newImage);
|
||||
$scope.$emit(events.IMAGE_METADATA_CHANGED, newMetadata);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
$scope.$apply();
|
||||
|
||||
expect(toast.add.calls.count()).toBe(1);
|
||||
expect($scope.$emit)
|
||||
.toHaveBeenCalledWith('horizon.app.core.images.UPDATE_SUCCESS', newImage);
|
||||
});
|
||||
|
||||
it('should destroy the event watchers', function() {
|
||||
testImage = { name: 'Test', id: '2' };
|
||||
var newImage = { name: 'Test2', id: '2' };
|
||||
var newMetadata = {p1: '11', p3: '3'};
|
||||
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
spyOn(glanceAPI, 'updateImage').and.callThrough();
|
||||
spyOn(metadataService, 'editMetadata').and.callThrough();
|
||||
spyOn(toast, 'add').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform(testImage);
|
||||
$scope.$apply();
|
||||
|
||||
$scope.$emit('$destroy');
|
||||
$scope.$emit(events.IMAGE_CHANGED, newImage);
|
||||
$scope.$emit(events.IMAGE_METADATA_CHANGED, newMetadata);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
$scope.$apply();
|
||||
|
||||
expect(glanceAPI.updateImage).toHaveBeenCalledWith(testImage);
|
||||
expect(metadataService.editMetadata)
|
||||
.toHaveBeenCalledWith('image', testImage.id, {}, ['p1', 'p2']);
|
||||
expect(toast.add.calls.count()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit', function() {
|
||||
it('should allow edit if image can be edited', function() {
|
||||
var image = {owner: 'project', status: 'active'};
|
||||
var allowed = service.allowed(image);
|
||||
permissionShouldPass(allowed);
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
it('should not allow edit if image is not owned by user', function() {
|
||||
deferred.reject();
|
||||
var image = {owner: 'doesnt_matter', status: 'active'};
|
||||
var allowed = service.allowed(image);
|
||||
permissionShouldFail(allowed);
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
it('should not allow edit if image status is not active', function() {
|
||||
var image = {owner: 'project', status: 'not_active'};
|
||||
var allowed = service.allowed(image);
|
||||
permissionShouldFail(allowed);
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
function permissionShouldFail(permissions) {
|
||||
permissions.then(
|
||||
function() {
|
||||
expect(false).toBe(true);
|
||||
},
|
||||
function() {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
function permissionShouldPass(permissions) {
|
||||
permissions.then(
|
||||
function() {
|
||||
expect(true).toBe(true);
|
||||
},
|
||||
function() {
|
||||
expect(false).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.factory('horizon.app.core.images.actions.editWorkflow', editWorkflow);
|
||||
|
||||
editWorkflow.$inject = [
|
||||
'horizon.app.core.images.basePath',
|
||||
'horizon.app.core.workflow.factory',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc factory
|
||||
* @name horizon.app.core.images.editWorkflow
|
||||
* @description A workflow for the edit image action.
|
||||
*/
|
||||
function editWorkflow(basePath, workflowService, gettext) {
|
||||
var workflow = workflowService({
|
||||
title: gettext('Edit Image'),
|
||||
btnText: { finish: gettext('Update Image') },
|
||||
steps: [
|
||||
{
|
||||
title: gettext('Image Details'),
|
||||
templateUrl: basePath + 'steps/edit-image/edit-image.html',
|
||||
helpUrl: basePath + 'steps/edit-image/edit-image.help.html',
|
||||
formName: 'imageForm'
|
||||
},
|
||||
{
|
||||
title: gettext('Metadata'),
|
||||
templateUrl: basePath + 'steps/update-metadata/update-metadata.html',
|
||||
helpUrl: basePath + 'steps/update-metadata/update-metadata.help.html',
|
||||
formName: 'updateMetadataForm'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
})();
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use self file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.images.actions.editWorkflow', function() {
|
||||
|
||||
var mockWorkflow = function(params) {
|
||||
return params;
|
||||
};
|
||||
|
||||
var service;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework.util'));
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.workflow', function($provide) {
|
||||
$provide.value('horizon.app.core.workflow.factory', mockWorkflow);
|
||||
}));
|
||||
|
||||
beforeEach(module('horizon.app.core.images', function($provide) {
|
||||
$provide.constant('horizon.app.core.images.basePath', '/dummy/');
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector) {
|
||||
service = $injector.get('horizon.app.core.images.actions.editWorkflow');
|
||||
}));
|
||||
|
||||
it('create the workflow for editing image', function() {
|
||||
expect(service.title).toEqual('Edit Image');
|
||||
var steps = service.steps;
|
||||
expect(steps.length).toEqual(2);
|
||||
expect(steps[0].templateUrl).toEqual('/dummy/steps/edit-image/edit-image.html');
|
||||
expect(steps[1].templateUrl).toEqual('/dummy/steps/update-metadata/update-metadata.html');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -29,6 +29,8 @@
|
||||
.module('horizon.app.core.images', ['ngRoute', 'horizon.app.core.images.actions'])
|
||||
.constant('horizon.app.core.images.events', events())
|
||||
.constant('horizon.app.core.images.non_bootable_image_types', ['aki', 'ari'])
|
||||
.constant('horizon.app.core.images.validationRules', validationRules())
|
||||
.constant('horizon.app.core.images.imageFormats', imageFormats())
|
||||
.constant('horizon.app.core.images.resourceType', 'OS::Glance::Image')
|
||||
.run(registerImageType)
|
||||
.config(config);
|
||||
@ -107,6 +109,39 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc constant
|
||||
* @name horizon.app.core.images.validationRules
|
||||
* @description constants for use in validation fields
|
||||
*/
|
||||
function validationRules() {
|
||||
return {
|
||||
integer: /^[0-9]+$/,
|
||||
fieldMaxLength: 255
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc constant
|
||||
* @name horizon.app.core.images.imageFormats
|
||||
* @description constants for list of image types in dropdowns
|
||||
*/
|
||||
function imageFormats() {
|
||||
return {
|
||||
iso: gettext('ISO - Optical Disk Image'),
|
||||
ova: gettext('OVA - Open Virtual Appliance'),
|
||||
qcow2: gettext('QCOW2 - QEMU Emulator'),
|
||||
raw: gettext('Raw'),
|
||||
vdi: gettext('VDI - Virtual Disk Image'),
|
||||
vhd: gettext('VHD - Virtual Hard Disk'),
|
||||
vmdk: gettext('VMDK - Virtual Machine Disk'),
|
||||
aki: gettext('AKI - Amazon Kernel Image'),
|
||||
ami: gettext('AMI - Amazon Machine Image'),
|
||||
ari: gettext('ARI - Amazon Ramdisk Image'),
|
||||
docker: gettext('Docker')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc value
|
||||
* @name horizon.app.core.images.events
|
||||
@ -116,7 +151,10 @@
|
||||
return {
|
||||
DELETE_SUCCESS: 'horizon.app.core.images.DELETE_SUCCESS',
|
||||
VOLUME_CHANGED: 'horizon.app.core.images.VOLUME_CHANGED',
|
||||
UPDATE_METADATA_SUCCESS: 'horizon.app.core.images.UPDATE_METADATA_SUCCESS'
|
||||
UPDATE_METADATA_SUCCESS: 'horizon.app.core.images.UPDATE_METADATA_SUCCESS',
|
||||
UPDATE_SUCCESS: 'horizon.app.core.images.UPDATE_SUCCESS',
|
||||
IMAGE_CHANGED: 'horizon.app.core.images.IMAGE_CHANGED',
|
||||
IMAGE_METADATA_CHANGED: 'horizon.app.core.images.IMAGE_METADATA_CHANGED'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,19 @@
|
||||
{ templateUrl: staticUrl + 'app/core/images/detail/image-detail.html'}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizon.app.core.images.imageFormats constant', function() {
|
||||
var imageFormats;
|
||||
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
imageFormats = $injector.get('horizon.app.core.images.imageFormats');
|
||||
}));
|
||||
|
||||
it('should be defined', function() {
|
||||
expect(Object.keys(imageFormats).length).toEqual(11);
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -0,0 +1,120 @@
|
||||
/**
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.controller('horizon.app.core.images.steps.EditImageController', EditImageController);
|
||||
|
||||
EditImageController.$inject = [
|
||||
'$scope',
|
||||
'horizon.app.core.images.events',
|
||||
'horizon.app.core.images.imageFormats',
|
||||
'horizon.app.core.images.validationRules',
|
||||
'horizon.app.core.openstack-service-api.settings'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name horizon.app.core.images.steps.EditImageController
|
||||
* @description
|
||||
* This controller is use for updating an image.
|
||||
*/
|
||||
function EditImageController(
|
||||
$scope,
|
||||
events,
|
||||
imageFormats,
|
||||
validationRules,
|
||||
settings
|
||||
) {
|
||||
var ctrl = this;
|
||||
|
||||
settings.getSettings().then(getConfiguredFormats);
|
||||
ctrl.diskFormats = [];
|
||||
ctrl.validationRules = validationRules;
|
||||
|
||||
ctrl.imageProtectedOptions = [
|
||||
{ label: gettext('Yes'), value: true },
|
||||
{ label: gettext('No'), value: false }
|
||||
];
|
||||
|
||||
ctrl.imageVisibilityOptions = [
|
||||
{ label: gettext('Public'), value: 'public'},
|
||||
{ label: gettext('Private'), value: 'private' }
|
||||
];
|
||||
|
||||
ctrl.setFormats = setFormats;
|
||||
|
||||
$scope.imagePromise.then(init);
|
||||
|
||||
var imageChangedWatcher;
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
imageChangedWatcher();
|
||||
});
|
||||
|
||||
///////////////////////////
|
||||
|
||||
function getConfiguredFormats(response) {
|
||||
var settingsFormats = response.OPENSTACK_IMAGE_FORMATS;
|
||||
var dupe = angular.copy(imageFormats);
|
||||
angular.forEach(dupe, function stripUnknown(name, key) {
|
||||
if (settingsFormats.indexOf(key) === -1) {
|
||||
delete dupe[key];
|
||||
}
|
||||
});
|
||||
|
||||
ctrl.imageFormats = dupe;
|
||||
}
|
||||
|
||||
function init(response) {
|
||||
ctrl.image = response.data;
|
||||
ctrl.image.kernel = ctrl.image.properties.kernel_id;
|
||||
ctrl.image.ramdisk = ctrl.image.properties.ramdisk_id;
|
||||
ctrl.image.architecture = ctrl.image.properties.architecture;
|
||||
ctrl.image.visibility = ctrl.image.is_public ? 'public' : 'private';
|
||||
ctrl.image_format = ctrl.image.disk_format;
|
||||
if (ctrl.image.container_format === 'docker') {
|
||||
ctrl.image_format = 'docker';
|
||||
ctrl.image.disk_format = 'raw';
|
||||
}
|
||||
setFormats();
|
||||
imageChangedWatcher = $scope.$watchCollection('ctrl.image', watchImageCollection);
|
||||
}
|
||||
|
||||
// emits new data to parent listeners
|
||||
function watchImageCollection(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
$scope.$emit(events.IMAGE_CHANGED, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
function setFormats() {
|
||||
ctrl.image.container_format = 'bare';
|
||||
if (['aki', 'ami', 'ari'].indexOf(ctrl.image_format) > -1) {
|
||||
ctrl.image.container_format = ctrl.image_format;
|
||||
}
|
||||
ctrl.image.disk_format = ctrl.image_format;
|
||||
if (ctrl.image_format === 'docker') {
|
||||
ctrl.image.container_format = 'docker';
|
||||
ctrl.image.disk_format = 'raw';
|
||||
}
|
||||
}
|
||||
|
||||
} // end of controller
|
||||
|
||||
})();
|
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.images edit image controller', function() {
|
||||
|
||||
var controller, $scope, events, $q, settingsCall, $timeout;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
|
||||
beforeEach(inject(function ($injector, _$rootScope_, _$q_, _$timeout_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
$q = _$q_;
|
||||
$timeout = _$timeout_;
|
||||
|
||||
events = $injector.get('horizon.app.core.images.events');
|
||||
controller = $injector.get('$controller');
|
||||
}));
|
||||
|
||||
function createController() {
|
||||
var settings = {
|
||||
getSettings: function() {
|
||||
settingsCall = $q.defer();
|
||||
return settingsCall.promise;
|
||||
}
|
||||
};
|
||||
return controller('horizon.app.core.images.steps.EditImageController as ctrl', {
|
||||
$scope: $scope,
|
||||
events: events,
|
||||
'horizon.app.core.openstack-service-api.settings': settings
|
||||
});
|
||||
}
|
||||
|
||||
function setImagePromise(image) {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({data: image});
|
||||
$scope.imagePromise = deferred.promise;
|
||||
}
|
||||
|
||||
it('should have options for visibility and protected', function() {
|
||||
setImagePromise({id: '1', container_format: 'bare', is_public: false, properties: []});
|
||||
var ctrl = createController();
|
||||
|
||||
expect(ctrl.imageVisibilityOptions.length).toEqual(2);
|
||||
expect(ctrl.imageProtectedOptions.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should map is_public', function() {
|
||||
setImagePromise({id: '1', container_format: 'bare', is_public: false, properties: []});
|
||||
var ctrl = createController();
|
||||
$timeout.flush();
|
||||
|
||||
expect(ctrl.image.visibility).toEqual('private');
|
||||
});
|
||||
|
||||
it('reads the data format settings', function() {
|
||||
setImagePromise({id: '1', container_format: 'bare', is_public: false, properties: []});
|
||||
var ctrl = createController();
|
||||
settingsCall.resolve({OPENSTACK_IMAGE_FORMATS: ['aki', 'ami']});
|
||||
$timeout.flush();
|
||||
expect(ctrl.imageFormats.aki).toBeDefined();
|
||||
expect(ctrl.imageFormats.ami).toBeDefined();
|
||||
expect(Object.keys(ctrl.imageFormats).length).toBe(2);
|
||||
});
|
||||
|
||||
it('should initialize image values correctly', function() {
|
||||
setImagePromise({
|
||||
id: '1',
|
||||
container_format: 'bare',
|
||||
disk_format: 'ova',
|
||||
is_public: true,
|
||||
properties: []
|
||||
});
|
||||
var ctrl = createController();
|
||||
$timeout.flush();
|
||||
|
||||
expect(ctrl.image.disk_format).toEqual('ova');
|
||||
expect(ctrl.image.visibility).toEqual('public');
|
||||
});
|
||||
|
||||
it('should set local image_format to docker when container is docker', function() {
|
||||
setImagePromise({id: '1', disk_format: 'raw', container_format: 'docker',
|
||||
is_public: false, properties: []});
|
||||
var ctrl = createController();
|
||||
$timeout.flush();
|
||||
|
||||
expect(ctrl.image_format).toEqual('docker');
|
||||
});
|
||||
|
||||
it('should set container to aki when disk format is same', function() {
|
||||
setImagePromise({id: '1', disk_format: 'aki', container_format: '',
|
||||
is_public: false, properties: []});
|
||||
var ctrl = createController();
|
||||
$timeout.flush();
|
||||
|
||||
expect(ctrl.image.container_format).toEqual('aki');
|
||||
});
|
||||
|
||||
it('should set container to ami when disk format is same', function() {
|
||||
setImagePromise({id: '1', disk_format: 'ami', container_format: '',
|
||||
is_public: false, properties: []});
|
||||
var ctrl = createController();
|
||||
$timeout.flush();
|
||||
|
||||
expect(ctrl.image.container_format).toEqual('ami');
|
||||
});
|
||||
|
||||
it('should set container to ari when disk format is same', function() {
|
||||
setImagePromise({id: '1', disk_format: 'ari', container_format: '',
|
||||
is_public: false, properties: []});
|
||||
var ctrl = createController();
|
||||
$timeout.flush();
|
||||
|
||||
expect(ctrl.image.container_format).toEqual('ari');
|
||||
});
|
||||
|
||||
it('should emit events on image change', function() {
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
|
||||
setImagePromise({id: '1', container_format: 'bare', properties: []});
|
||||
var ctrl = createController();
|
||||
ctrl.image = 1;
|
||||
$scope.$apply();
|
||||
|
||||
ctrl.image = 2;
|
||||
$scope.$apply();
|
||||
|
||||
expect($scope.$emit).toHaveBeenCalledWith('horizon.app.core.images.IMAGE_CHANGED', 2);
|
||||
});
|
||||
|
||||
it("should destroy the image changed watcher when the controller is destroyed", function() {
|
||||
setImagePromise({id: '1', container_format: 'bare', properties: []});
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
|
||||
var ctrl = createController();
|
||||
ctrl.image = 1;
|
||||
$scope.$apply();
|
||||
|
||||
$scope.$emit("$destroy");
|
||||
$scope.$emit.calls.reset();
|
||||
|
||||
ctrl.image = 2;
|
||||
$scope.$apply();
|
||||
|
||||
expect($scope.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<h3 translate>Description</h3>
|
||||
<p translate>Use this page to edit information about the image.</p>
|
||||
</div>
|
@ -0,0 +1,147 @@
|
||||
<div ng-controller="horizon.app.core.images.steps.EditImageController as ctrl">
|
||||
|
||||
<div class="content">
|
||||
|
||||
<h3 translate>Image Detail</h3>
|
||||
|
||||
<div class="selected-source">
|
||||
<div class="row form-group">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error':imageForm.name.$invalid && imageForm.name.$dirty}">
|
||||
<label class="control-label required" for="imageForm-name" translate>Image Name</label>
|
||||
<input required
|
||||
id="imageForm-name" name="name"
|
||||
type="text" class="form-control"
|
||||
ng-model="ctrl.image.name"
|
||||
ng-maxlength="ctrl.validationRules.fieldMaxLength"
|
||||
placeholder="{$ 'Enter an Image name'|translate $}">
|
||||
<p class="help-block"
|
||||
ng-show="imageForm.name.$invalid && imageForm.name.$dirty">
|
||||
<translate>An image name less than 256 characters is required.</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error':imageForm.description.$invalid && imageForm.description.$dirty}">
|
||||
<label class="control-label" for="imageForm-description" translate>Image Description</label>
|
||||
<input id="imageForm-description" name="description"
|
||||
type="text" class="form-control"
|
||||
ng-model="ctrl.image.properties.description"
|
||||
ng-maxlength="ctrl.validationRules.fieldMaxLength"
|
||||
placeholder="{$ 'Enter an Image Description'|translate $}">
|
||||
<p class="help-block alert alert-danger"
|
||||
ng-show="imageForm.description.$invalid && imageForm.description.$dirty">
|
||||
<translate>An image description less than 256 characters is required.</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-source clearfix">
|
||||
<div class="row form-group">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group required">
|
||||
<label class="control-label required" for="imageForm-format" translate>Format</label>
|
||||
<select class="form-control switchable ng-pristine ng-untouched ng-valid" ng-required="true" id="imageForm-format" name="format" ng-model="ctrl.image_format" ng-options="key as label for (key, label) in ctrl.imageFormats" ng-change="ctrl.setFormats()">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 translate>Image Requirements</h3>
|
||||
<div class="subtitle"></div>
|
||||
|
||||
<div class="selected-source clearfix">
|
||||
<div class="row form-group">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group" for="imageForm-kernel">
|
||||
<label class="control-label" translate>Kernel ID</label>
|
||||
<input id="imageForm-kernel" name="kernel"
|
||||
type="text" class="form-control"
|
||||
readonly="readonly"
|
||||
ng-model="ctrl.image.kernel">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="imageForm-ramdisk" translate>Ramdisk ID</label>
|
||||
<input id="imageForm-ramdisk" name="ramdisk"
|
||||
type="text" class="form-control"
|
||||
readonly="readonly"
|
||||
ng-model="ctrl.image.ramdisk">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="imageForm-architecture" translate>Architecture</label>
|
||||
<input id="imageForm-architecture" name="architecture"
|
||||
type="text" class="form-control"
|
||||
ng-model="ctrl.image.architecture"
|
||||
readonly="readonly">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error':imageForm.min_disk.$invalid && imageForm.min_disk.$dirty}">
|
||||
<label class="control-label" for="imageForm-min_disk" translate>Minimum Disk (GB)</label>
|
||||
<input id="imageForm-min_disk" name="min_disk"
|
||||
type="number" class="form-control"
|
||||
ng-pattern="ctrl.validationRules.integer" ng-model="ctrl.image.min_disk"
|
||||
placeholder="{$ 'The minimum disk size required to boot the image. If unspecified, this value defaults to 0 (no minimum).'|translate $}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error':imageForm.min_ram.$invalid && imageForm.min_ram.$dirty}">
|
||||
<label class="control-label required" translate>Minimum RAM (MB)</label>
|
||||
<input id="imageForm-min_ram" name="min_ram"
|
||||
type="number" class="form-control"
|
||||
ng-pattern="ctrl.validationRules.integer" ng-model="ctrl.image.min_ram"
|
||||
placeholder="{$ 'The minimum memory size required to boot the image. If unspecified, this value defaults to 0 (no minimum).'|translate $}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 translate>Image Sharing</h3>
|
||||
<div class="subtitle"></div>
|
||||
|
||||
<div class="selected-source clearfix">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label required" translate>Visibility</label>
|
||||
<div class="form-field">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-toggle"
|
||||
ng-repeat="option in ctrl.imageVisibilityOptions"
|
||||
ng-model="ctrl.image.visibility"
|
||||
btn-radio="option.value">{$ ::option.label $}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label required" translate>Protected</label>
|
||||
<div class="form-field">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-toggle"
|
||||
ng-repeat="option in ctrl.imageProtectedOptions"
|
||||
ng-model="ctrl.image.protected"
|
||||
btn-radio="option.value">{$ ::option.label $}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.controller('horizon.app.core.images.steps.UpdateMetadataController', UpdateMetadataController);
|
||||
|
||||
UpdateMetadataController.$inject = [
|
||||
'$scope',
|
||||
'$q',
|
||||
'horizon.app.core.images.events',
|
||||
'horizon.app.core.metadata.service',
|
||||
'horizon.framework.widgets.metadata.tree.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name MetadataController
|
||||
* @description
|
||||
* Controller used by Image for Update Metadata
|
||||
*/
|
||||
function UpdateMetadataController(
|
||||
$scope,
|
||||
$q,
|
||||
events,
|
||||
metadataService,
|
||||
metadataTreeService
|
||||
) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.tree = new metadataTreeService.Tree([], []);
|
||||
|
||||
/* eslint-enable angular/ng_controller_as */
|
||||
$scope.$watchCollection(getTree, onMetadataChanged);
|
||||
/* eslint-enable angular/ng_controller_as */
|
||||
|
||||
$scope.imagePromise.then(init);
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
function init(response) {
|
||||
var image = response.data;
|
||||
$q.all({
|
||||
available: metadataService.getNamespaces('image'),
|
||||
existing: metadataService.getMetadata('image', image.id)
|
||||
}).then(onMetadataGet);
|
||||
}
|
||||
|
||||
function onMetadataGet(response) {
|
||||
ctrl.tree = new metadataTreeService.Tree(
|
||||
response.available.data.items,
|
||||
response.existing.data
|
||||
);
|
||||
}
|
||||
|
||||
function getTree() {
|
||||
return ctrl.tree.getExisting();
|
||||
}
|
||||
|
||||
function onMetadataChanged(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
$scope.$emit(events.IMAGE_METADATA_CHANGED, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.images', function () {
|
||||
|
||||
describe('controller.UpdateMetadataController', function () {
|
||||
var mockTree = {
|
||||
getExisting: function() {}
|
||||
};
|
||||
|
||||
var availableMetadata = {id: 'availableMetadata'};
|
||||
var existingMetadata = {id: 'existingMetadata'};
|
||||
|
||||
var metadataService = {
|
||||
getNamespaces: function() {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback({
|
||||
data: {
|
||||
items: availableMetadata
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
getMetadata: function() {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback({
|
||||
data: existingMetadata
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var metadataTreeService = {
|
||||
Tree: function() {}
|
||||
};
|
||||
|
||||
var $controller, $scope, $q;
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.framework.widgets'));
|
||||
beforeEach(module('horizon.framework.widgets.metadata.tree'));
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('horizon.framework.widgets.metadata.tree.service', metadataTreeService);
|
||||
$provide.value('horizon.app.core.metadata.service', metadataService);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
|
||||
$controller = $injector.get('$controller');
|
||||
$scope = _$rootScope_.$new();
|
||||
$q = _$q_;
|
||||
}));
|
||||
|
||||
it('should setup up the metadata tree', function() {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({data: {id: '1'}});
|
||||
$scope.imagePromise = deferred.promise;
|
||||
|
||||
spyOn(metadataTreeService, 'Tree').and.returnValue(mockTree);
|
||||
spyOn(metadataService, 'getNamespaces').and.callThrough();
|
||||
spyOn(metadataService, 'getMetadata').and.callThrough();
|
||||
|
||||
var ctrl = createController();
|
||||
$scope.$apply();
|
||||
|
||||
expect(ctrl.tree).toEqual(mockTree);
|
||||
expect(metadataTreeService.Tree).toHaveBeenCalledWith([], []);
|
||||
expect(metadataTreeService.Tree).toHaveBeenCalledWith(availableMetadata, existingMetadata);
|
||||
});
|
||||
|
||||
it('should emit imageMetadataChanged event when metadata changes', function() {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({data: {id: '1'}});
|
||||
$scope.imagePromise = deferred.promise;
|
||||
spyOn(metadataTreeService, 'Tree').and.returnValue(mockTree);
|
||||
|
||||
createController();
|
||||
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
var mockGetExisting = spyOn(mockTree, 'getExisting');
|
||||
|
||||
mockGetExisting.and.returnValue('1');
|
||||
$scope.$apply();
|
||||
|
||||
mockGetExisting.and.returnValue('2');
|
||||
$scope.$apply();
|
||||
|
||||
expect($scope.$emit).toHaveBeenCalledWith(
|
||||
'horizon.app.core.images.IMAGE_METADATA_CHANGED',
|
||||
'2'
|
||||
);
|
||||
});
|
||||
|
||||
function createController() {
|
||||
return $controller('horizon.app.core.images.steps.UpdateMetadataController', {
|
||||
'$scope': $scope,
|
||||
'metadataService': metadataService,
|
||||
'metadataTreeService': metadataTreeService
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
})();
|
@ -0,0 +1,16 @@
|
||||
<div>
|
||||
<h1 translate>Metadata Help</h1>
|
||||
<p translate>You can add arbitrary metadata to your image.</p>
|
||||
<p translate>
|
||||
Metadata is used to provide additional information about the
|
||||
image. Sometimes this information is only used for sorting and viewing.
|
||||
In some installations this information may affect how the instance is
|
||||
deployed or behaves.
|
||||
</p>
|
||||
<p translate>
|
||||
Metadata is a collection of key-value pairs associated with an instance.
|
||||
The maximum length for each metadata key and value is 255 characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
<div ng-controller="horizon.app.core.images.steps.UpdateMetadataController as metadataCtrl">
|
||||
<h1 translate>Image Metadata</h1>
|
||||
<div class="content">
|
||||
<metadata-tree model="metadataCtrl.tree" form="imageForm"></metadata-tree>
|
||||
</div>
|
||||
</div>
|
@ -65,6 +65,7 @@
|
||||
ctrl.imageResourceType = typeRegistry.getResourceType(imageResourceType);
|
||||
|
||||
var deleteWatcher = $scope.$on(events.DELETE_SUCCESS, onDeleteSuccess);
|
||||
var updateWatcher = $scope.$on(events.UPDATE_SUCCESS, onUpdateSuccess);
|
||||
|
||||
$scope.$on('$destroy', destroy);
|
||||
|
||||
@ -95,6 +96,12 @@
|
||||
applyMetadataDefinitions();
|
||||
}
|
||||
|
||||
function onUpdateSuccess(e, image) {
|
||||
e.stopPropagation();
|
||||
ctrl.imagesSrc = difference(ctrl.imagesSrc, [image.id], 'id');
|
||||
ctrl.imagesSrc.push(image);
|
||||
}
|
||||
|
||||
function onDeleteSuccess(e, removedImageIds) {
|
||||
ctrl.imagesSrc = difference(ctrl.imagesSrc, removedImageIds, 'id');
|
||||
e.stopPropagation();
|
||||
@ -115,6 +122,7 @@
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
updateWatcher();
|
||||
deleteWatcher();
|
||||
}
|
||||
|
||||
|
@ -18,15 +18,20 @@
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.images table controller', function() {
|
||||
var images = [{id: '1', visibility: 'public', filtered_visibility: 'Public'},
|
||||
{id: '2', is_public: false, owner: 'not_me', filtered_visibility: 'Shared with Me'}];
|
||||
|
||||
var glanceAPI = {
|
||||
getImages: function () {
|
||||
return {
|
||||
data: {
|
||||
items: [
|
||||
{id: '1', visibility: 'public'},
|
||||
{id: '2', is_public: false, owner: 'not_me'}
|
||||
{id: '1', visibility: 'public', filtered_visibility: 'Public'},
|
||||
{id: '2', is_public: false, owner: 'not_me', filtered_visibility: 'Shared with Me'}
|
||||
]
|
||||
},
|
||||
success: function(callback) {
|
||||
callback({items : angular.copy(images)});
|
||||
}
|
||||
};
|
||||
},
|
||||
@ -133,7 +138,16 @@
|
||||
expect($scope.$emit).toHaveBeenCalledWith('hzTable:clearSelected');
|
||||
});
|
||||
|
||||
it('should destroy the event watchers', function() {
|
||||
it('should refresh images after update', function() {
|
||||
var ctrl = createController();
|
||||
expect(ctrl.imagesSrc).toEqual(images);
|
||||
|
||||
$scope.$emit(events.UPDATE_SUCCESS, {id: '1', name: 'name_new'});
|
||||
|
||||
expect(ctrl.imagesSrc.filter(function (x) { return x.id === '1'; })[0].name).toBe('name_new');
|
||||
});
|
||||
|
||||
it('should destroy the event watcher for delete', function() {
|
||||
var ctrl = createController();
|
||||
|
||||
$scope.$emit('$destroy');
|
||||
@ -145,5 +159,23 @@
|
||||
]);
|
||||
});
|
||||
|
||||
it('should destroy the event watcher for update', function() {
|
||||
var ctrl = createController();
|
||||
|
||||
$scope.$emit('$destroy');
|
||||
$scope.$emit(events.UPDATE_SUCCESS, {id: '1', name: 'name_new'});
|
||||
|
||||
expect(ctrl.imagesSrc).toEqual(images);
|
||||
});
|
||||
|
||||
it('should destroy the event watcher for create', function() {
|
||||
var ctrl = createController();
|
||||
|
||||
$scope.$emit('$destroy');
|
||||
$scope.$emit(events.createSuccess, {id: '3'});
|
||||
|
||||
expect(ctrl.imagesSrc).toEqual(images);
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
||||
|
Loading…
Reference in New Issue
Block a user