Adding Create Image Action to angular images panel
Adds the ability to create an image from the table. Image File Upload is not currently supported. To test set DISABLED = False in _1051_project_ng_images_panel.py Co-Authored-By: Nathan Zeplowitz<nzeplowi@thoughtworks.com> Co-Authored-By: Kyle Olivo<keolivo@thoughtworks.com> Co-Authored-By: Errol Pais<epais@thoughtworks.com> Co-Authored-By: Matt Borland <matt.borland@hpe.com> Co-Authored-By: Tyr Johanson <tyr@hpe.com> Closes-Bug: 1580206 Change-Id: I2f49b4f8c0d82b03289bb44b8c6fddd70ee67bae Partially-Implements: blueprint angularize-images-table
This commit is contained in:
parent
4f9e33d4c5
commit
6ccd9fb6b4
|
@ -30,6 +30,7 @@
|
|||
registerImageActions.$inject = [
|
||||
'horizon.framework.conf.resource-type-registry.service',
|
||||
'horizon.app.core.images.actions.edit.service',
|
||||
'horizon.app.core.images.actions.create.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',
|
||||
|
@ -40,6 +41,7 @@
|
|||
function registerImageActions(
|
||||
registry,
|
||||
editService,
|
||||
createService,
|
||||
createVolumeService,
|
||||
deleteImageService,
|
||||
launchInstanceService,
|
||||
|
@ -86,6 +88,14 @@
|
|||
});
|
||||
|
||||
imageResourceType.batchActions
|
||||
.append({
|
||||
id: 'createImageAction',
|
||||
service: createService,
|
||||
template: {
|
||||
text: gettext('Create Image'),
|
||||
type: 'create'
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'batchDeleteImageAction',
|
||||
service: deleteImageService,
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* (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';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.factory('horizon.app.core.images.actions.create.service', createService);
|
||||
|
||||
createService.$inject = [
|
||||
'$q',
|
||||
'horizon.app.core.images.events',
|
||||
'horizon.app.core.images.resourceType',
|
||||
'horizon.app.core.images.actions.createWorkflow',
|
||||
'horizon.app.core.metadata.service',
|
||||
'horizon.app.core.openstack-service-api.glance',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.framework.util.actions.action-result.service',
|
||||
'horizon.framework.widgets.modal.wizard-modal.service',
|
||||
'horizon.framework.widgets.toast.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.app.core.images.actions.createService
|
||||
* @Description A service to open the user wizard.
|
||||
*/
|
||||
function createService(
|
||||
$q,
|
||||
events,
|
||||
resourceType,
|
||||
createWorkflow,
|
||||
metadataService,
|
||||
glance,
|
||||
policy,
|
||||
actionResultService,
|
||||
wizardModalService,
|
||||
toast
|
||||
) {
|
||||
var message = {
|
||||
success: gettext('Image %s was successfully created.')
|
||||
};
|
||||
|
||||
var model = {};
|
||||
|
||||
var scope;
|
||||
|
||||
var service = {
|
||||
initScope: initScope,
|
||||
perform: perform,
|
||||
allowed: allowed
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
function initScope($scope) {
|
||||
var watchImageChange = $scope.$on(events.IMAGE_CHANGED, onImageChange);
|
||||
var watchMetadataChange = $scope.$on(events.IMAGE_METADATA_CHANGED, onMetadataChange);
|
||||
|
||||
scope = $scope;
|
||||
|
||||
$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() {
|
||||
return policy.ifAllowed({ rules: [['image', 'add_image']] });
|
||||
}
|
||||
|
||||
function perform() {
|
||||
model.image = {};
|
||||
model.metadata = {};
|
||||
scope.image = {};
|
||||
|
||||
return wizardModalService.modal({
|
||||
scope: scope,
|
||||
workflow: createWorkflow,
|
||||
submit: submit
|
||||
}).result;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
var finalModel = angular.extend({}, model.image, model.metadata);
|
||||
return glance.createImage(finalModel).then(onCreateImage);
|
||||
}
|
||||
|
||||
function onCreateImage(response) {
|
||||
var newImage = response.data;
|
||||
toast.add('success', interpolate(message.success, [newImage.name]));
|
||||
return actionResultService.getActionResult()
|
||||
.created(resourceType, newImage.id)
|
||||
.result;
|
||||
}
|
||||
|
||||
} // end of createService
|
||||
})(); // end of IIFE
|
|
@ -0,0 +1,183 @@
|
|||
/**
|
||||
* (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.actions.create.service', function() {
|
||||
var metadataService = {
|
||||
editMetadata: function() {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var wizardModalService = {
|
||||
modal: function (config) {
|
||||
deferredModal = $q.defer();
|
||||
deferredModal.resolve(config.scope.image);
|
||||
return {result: deferredModal.promise};
|
||||
}
|
||||
};
|
||||
|
||||
var glanceAPI = {
|
||||
createImage: function(image) {
|
||||
deferredCreate = $q.defer();
|
||||
deferredCreate.resolve({data: image});
|
||||
return deferredCreate.promise;
|
||||
}
|
||||
};
|
||||
|
||||
var policyAPI = {
|
||||
ifAllowed: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({allowed: true});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var service, events, $scope, toast, deferredModal, deferredCreate, $q;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('horizon.framework.widgets.modal.wizard-modal.service', wizardModalService);
|
||||
$provide.value('horizon.app.core.openstack-service-api.glance', glanceAPI);
|
||||
$provide.value('horizon.app.core.openstack-service-api.policy', policyAPI);
|
||||
$provide.value('horizon.app.core.metadata.service', metadataService);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
service = $injector.get('horizon.app.core.images.actions.create.service');
|
||||
events = $injector.get('horizon.app.core.images.events');
|
||||
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||
$q = _$q_;
|
||||
}));
|
||||
|
||||
it('should check the policy if the user is allowed to delete images', function() {
|
||||
spyOn(policyAPI, 'ifAllowed').and.callThrough();
|
||||
var allowed = service.allowed();
|
||||
expect(allowed).toBeTruthy();
|
||||
expect(policyAPI.ifAllowed).toHaveBeenCalledWith({ rules: [['image', 'add_image']] });
|
||||
});
|
||||
|
||||
it('open the modal with the correct parameters', function() {
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
|
||||
expect(wizardModalService.modal).toHaveBeenCalled();
|
||||
expect($scope.image).toEqual({});
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
expect(modalArgs.scope).toEqual($scope);
|
||||
expect(modalArgs.workflow).toBeDefined();
|
||||
expect(modalArgs.submit).toBeDefined();
|
||||
});
|
||||
|
||||
it('should submit create image request to glance', function() {
|
||||
var image = { name: 'Test', id: '2' };
|
||||
var newMetadata = {prop1: '11', prop3: '3'};
|
||||
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(glanceAPI, 'createImage').and.callThrough();
|
||||
spyOn(toast, 'add').and.callThrough();
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
|
||||
$scope.$emit(events.IMAGE_CHANGED, image);
|
||||
$scope.$emit(events.IMAGE_METADATA_CHANGED, newMetadata);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
$scope.$apply();
|
||||
|
||||
expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test',
|
||||
id: '2', prop1: '11', prop3: '3'});
|
||||
expect(toast.add).toHaveBeenCalledWith('success', 'Image Test was successfully created.');
|
||||
});
|
||||
|
||||
it('should raise event even if update meta data fails', function() {
|
||||
var image = { name: 'Test', id: '2' };
|
||||
var failedPromise = function() {
|
||||
return {
|
||||
then: function(callback, errorCallback) {
|
||||
errorCallback();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
spyOn(glanceAPI, 'createImage').and.callThrough();
|
||||
spyOn(metadataService, 'editMetadata').and.callFake(failedPromise);
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(toast, 'add').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
$scope.$apply();
|
||||
|
||||
$scope.$emit(events.IMAGE_CHANGED, image);
|
||||
$scope.$emit(events.IMAGE_METADATA_CHANGED, newMetadata);
|
||||
|
||||
var newMetadata = {prop1: '11', prop3: '3'};
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
$scope.$apply();
|
||||
|
||||
expect(toast.add.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('should destroy the event watchers', function() {
|
||||
var newImage = { name: 'Test2', id: '2' };
|
||||
var newMetadata = {p1: '11', p3: '3'};
|
||||
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
spyOn(glanceAPI, 'createImage').and.callThrough();
|
||||
spyOn(metadataService, 'editMetadata').and.callThrough();
|
||||
spyOn(toast, 'add').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
$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.createImage).toHaveBeenCalledWith({});
|
||||
expect(toast.add.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* (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';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.factory('horizon.app.core.images.actions.createWorkflow', createWorkflow);
|
||||
|
||||
createWorkflow.$inject = [
|
||||
'horizon.app.core.images.basePath',
|
||||
'horizon.app.core.workflow.factory',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc factory
|
||||
* @name horizon.app.core.images.createWorkflow
|
||||
* @description A workflow for the create image action.
|
||||
*/
|
||||
function createWorkflow(basePath, workflowService, gettext) {
|
||||
var workflow = workflowService({
|
||||
title: gettext('Create Image'),
|
||||
btnText: { finish: gettext('Create Image') },
|
||||
steps: [
|
||||
{
|
||||
title: gettext('Image Details'),
|
||||
templateUrl: basePath + 'steps/create-image/create-image.html',
|
||||
helpUrl: basePath + 'steps/create-image/create-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,56 @@
|
|||
/**
|
||||
* (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.actions.createWorkflow', 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.createWorkflow');
|
||||
}));
|
||||
|
||||
it('create the workflow for creating image', function() {
|
||||
expect(service.title).toEqual('Create Image');
|
||||
expect(service.steps.length).toEqual(2);
|
||||
expect(service.steps[0].templateUrl).toEqual('/dummy/steps/create-image/create-image.html');
|
||||
expect(service.steps[1].templateUrl).toEqual(
|
||||
'/dummy/steps/update-metadata/update-metadata.html'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
|
@ -1,4 +1,6 @@
|
|||
/**
|
||||
* (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
|
||||
|
@ -101,12 +103,16 @@
|
|||
scope: scope,
|
||||
workflow: editWorkflow,
|
||||
submit: submit
|
||||
});
|
||||
}).result.catch(cancel);
|
||||
|
||||
saveDeferred = $q.defer();
|
||||
return saveDeferred.promise;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
saveDeferred.reject();
|
||||
}
|
||||
|
||||
function submit() {
|
||||
return saveMetadata().then(onSaveMetadata, onFailMetadata);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* 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
|
||||
* 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
|
||||
|
@ -39,7 +41,7 @@
|
|||
|
||||
var wizardModalService = {
|
||||
modal: function () {
|
||||
return { result: {} };
|
||||
return { result: {catch: angular.noop} };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
* (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
|
||||
|
@ -25,7 +25,6 @@
|
|||
describe('horizon.app.core.images.tableRoute constant', function () {
|
||||
var tableRoute;
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
tableRoute = $injector.get('horizon.app.core.images.tableRoute');
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* (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';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.controller('horizon.app.core.images.steps.CreateImageController', CreateImageController);
|
||||
|
||||
CreateImageController.$inject = [
|
||||
'$scope',
|
||||
'horizon.app.core.openstack-service-api.glance',
|
||||
'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.CreateImageController
|
||||
* @description
|
||||
* This controller is use for creating an image.
|
||||
*/
|
||||
function CreateImageController(
|
||||
$scope,
|
||||
glance,
|
||||
events,
|
||||
imageFormats,
|
||||
validationRules,
|
||||
settings
|
||||
) {
|
||||
var ctrl = this;
|
||||
|
||||
settings.getSettings().then(getConfiguredFormats);
|
||||
ctrl.validationRules = validationRules;
|
||||
ctrl.imageFormats = imageFormats;
|
||||
ctrl.diskFormats = [];
|
||||
|
||||
ctrl.image = {
|
||||
source_type: 'url',
|
||||
image_url: '',
|
||||
is_copying: true,
|
||||
protected: false,
|
||||
min_disk: 0,
|
||||
min_ram: 0,
|
||||
container_format: '',
|
||||
disk_format: '',
|
||||
visibility: 'public'
|
||||
};
|
||||
|
||||
ctrl.imageProtectedOptions = [
|
||||
{ label: gettext('Yes'), value: true },
|
||||
{ label: gettext('No'), value: false }
|
||||
];
|
||||
|
||||
ctrl.imageCopyOptions = [
|
||||
{ label: gettext('Yes'), value: true },
|
||||
{ label: gettext('No'), value: false }
|
||||
];
|
||||
|
||||
ctrl.imageVisibilityOptions = [
|
||||
{ label: gettext('Public'), value: 'public'},
|
||||
{ label: gettext('Private'), value: 'private' }
|
||||
];
|
||||
|
||||
ctrl.kernelImages = [];
|
||||
ctrl.ramdiskImages = [];
|
||||
|
||||
ctrl.setFormats = setFormats;
|
||||
|
||||
init();
|
||||
|
||||
var imageChangedWatcher = $scope.$watchCollection('ctrl.image', watchImageCollection);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
// emits new data to parent listeners
|
||||
function watchImageCollection(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
$scope.$emit(events.IMAGE_CHANGED, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
glance.getImages({paginate: false}).success(onGetImages);
|
||||
}
|
||||
|
||||
function onGetImages(response) {
|
||||
ctrl.kernelImages = response.items.filter(function(elem) {
|
||||
return elem.disk_format === 'aki';
|
||||
});
|
||||
|
||||
ctrl.ramdiskImages = response.items.filter(function(elem) {
|
||||
return elem.disk_format === 'ari';
|
||||
});
|
||||
}
|
||||
|
||||
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,184 @@
|
|||
/**
|
||||
* (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 create image controller', function() {
|
||||
|
||||
function fakeGlance() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({
|
||||
items: [
|
||||
{disk_format: 'aki'},
|
||||
{disk_format: 'ari'},
|
||||
{disk_format: ''}]
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var controller, glanceAPI, $scope, events, $q, settingsCall, $timeout;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.app.core'));
|
||||
|
||||
beforeEach(inject(function ($injector, _$rootScope_, _$q_, _$timeout_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
$q = _$q_;
|
||||
$timeout = _$timeout_;
|
||||
|
||||
glanceAPI = $injector.get('horizon.app.core.openstack-service-api.glance');
|
||||
|
||||
events = $injector.get('horizon.app.core.images.events');
|
||||
controller = $injector.get('$controller');
|
||||
|
||||
spyOn(glanceAPI, 'getImages').and.callFake(fakeGlance);
|
||||
}));
|
||||
|
||||
function createController() {
|
||||
var settings = {
|
||||
getSettings: function() {
|
||||
settingsCall = $q.defer();
|
||||
return settingsCall.promise;
|
||||
}
|
||||
};
|
||||
var imageFormats = {
|
||||
'a': 'apple',
|
||||
'b': 'banana',
|
||||
'c': 'cherry',
|
||||
'd': 'django'
|
||||
};
|
||||
return controller('horizon.app.core.images.steps.CreateImageController as ctrl', {
|
||||
$scope: $scope,
|
||||
glanceAPI: glanceAPI,
|
||||
events: events,
|
||||
'horizon.app.core.openstack-service-api.settings': settings,
|
||||
'horizon.app.core.images.imageFormats': imageFormats
|
||||
});
|
||||
}
|
||||
|
||||
it('should call glance API on init', function() {
|
||||
var ctrl = createController();
|
||||
|
||||
expect(glanceAPI.getImages).toHaveBeenCalledWith({paginate: false});
|
||||
expect(ctrl.kernelImages).toEqual([{disk_format: 'aki'}]);
|
||||
expect(ctrl.ramdiskImages).toEqual([{disk_format: 'ari'}]);
|
||||
});
|
||||
|
||||
it('should emit events on image change', function() {
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
|
||||
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 have options for visibility, protected and copying', function() {
|
||||
var ctrl = createController();
|
||||
|
||||
expect(ctrl.imageVisibilityOptions.length).toEqual(2);
|
||||
expect(ctrl.imageProtectedOptions.length).toEqual(2);
|
||||
expect(ctrl.imageCopyOptions.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("should destroy the image changed watcher when the controller is destroyed", function() {
|
||||
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();
|
||||
});
|
||||
|
||||
it("should set the default values", function() {
|
||||
var ctrl = createController();
|
||||
|
||||
expect(ctrl.imageFormats).toBeDefined();
|
||||
expect(ctrl.validationRules).toBeDefined();
|
||||
expect(ctrl.diskFormats).toEqual([]);
|
||||
expect(ctrl.image.visibility).toEqual('public');
|
||||
expect(ctrl.image.min_disk).toEqual(0);
|
||||
expect(ctrl.image.min_ram).toEqual(0);
|
||||
});
|
||||
|
||||
describe('setFormats', function() {
|
||||
var ctrl;
|
||||
beforeEach(function() {
|
||||
ctrl = createController();
|
||||
});
|
||||
|
||||
it('assumes bare container format', function() {
|
||||
ctrl.image_format = 'unknown';
|
||||
ctrl.setFormats();
|
||||
expect(ctrl.image.container_format).toBe('bare');
|
||||
});
|
||||
|
||||
it('uses the given image format', function() {
|
||||
ctrl.image_format = 'unknown';
|
||||
ctrl.setFormats();
|
||||
expect(ctrl.image.disk_format).toBe('unknown');
|
||||
});
|
||||
|
||||
it('sets container to ami/aki/ari if format is ami/aki/ari', function() {
|
||||
['ami', 'aki', 'ari'].forEach(function(format) {
|
||||
ctrl.image_format = format;
|
||||
ctrl.setFormats();
|
||||
expect(ctrl.image.disk_format).toBe(format);
|
||||
expect(ctrl.image.container_format).toBe(format);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets docker/raw for container/disk if type is docker', function() {
|
||||
ctrl.image_format = 'docker';
|
||||
ctrl.setFormats();
|
||||
expect(ctrl.image.disk_format).toBe('raw');
|
||||
expect(ctrl.image.container_format).toBe('docker');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfiguredFormats', function() {
|
||||
|
||||
it('uses the settings for the source of allowed image formats', function() {
|
||||
var ctrl = createController();
|
||||
settingsCall.resolve({OPENSTACK_IMAGE_FORMATS: ['a', 'b', 'c']});
|
||||
$timeout.flush();
|
||||
var expectation = {
|
||||
'a': 'apple',
|
||||
'b': 'banana',
|
||||
'c': 'cherry'
|
||||
};
|
||||
expect(ctrl.imageFormats).toEqual(expectation);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,10 @@
|
|||
<div>
|
||||
<h3 translate>Description:</h3>
|
||||
<p translate>
|
||||
Currently only images available via an HTTP URL are supported. The image location must be accessible to the Image Service. Compressed image binaries are supported (.zip and .tar.gz.)
|
||||
</p>
|
||||
<p>
|
||||
<strong translate>Please note: </strong>
|
||||
<translate>The Image Location field MUST be a valid and direct URL to the image binary. URLs that redirect or serve error pages will result in unusable images.</translate>
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,208 @@
|
|||
<div ng-controller="horizon.app.core.images.steps.CreateImageController as ctrl">
|
||||
|
||||
<h3 translate>Image Details</h3>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="subtitle">
|
||||
<translate>
|
||||
Specify an image to upload to the Image Service.
|
||||
</translate>
|
||||
</div>
|
||||
|
||||
<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</translate><span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<input required id="imageForm-name" name="name"
|
||||
type="text" class="form-control"
|
||||
ng-model="ctrl.image.name"
|
||||
ng-maxlength="ctrl.validationRules.fieldMaxLength">
|
||||
<p class="help-block alert alert-danger"
|
||||
ng-show="imageForm.name.$invalid && imageForm.name.$dirty">
|
||||
<translate>An image name less than {$ctrl.validationRules.fieldMaxLength + 1$} 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</translate>
|
||||
</label>
|
||||
<input id="imageForm-description" name="description"
|
||||
type="text" class="form-control"
|
||||
ng-model="ctrl.image.description"
|
||||
ng-maxlength="ctrl.validationRules.fieldMaxLength">
|
||||
<p class="help-block alert alert-danger"
|
||||
ng-show="imageForm.description.$invalid && imageForm.description.$dirty">
|
||||
<translate>An image description less than {$ctrl.validationRules.fieldMaxLength + 1$} characters is required.</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title" translate>Image Source</h3>
|
||||
<div class="subtitle"></div>
|
||||
|
||||
<div class="selected-source">
|
||||
<div class="row form-group">
|
||||
<div class="col-xs-6 col-sm-6" ng-if="ctrl.image.source_type === 'url'">
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error':imageForm.image_url.$invalid && imageForm.image_url.$dirty}">
|
||||
<label class="control-label" for="imageForm-image_url">
|
||||
<translate>Location</translate><span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<input ng-required="true" id="imageForm-image_url" name="image_url"
|
||||
type="text" class="form-control"
|
||||
ng-model="ctrl.image.image_url"
|
||||
ng-maxlength="ctrl.validationRules.fieldMaxLength"
|
||||
placeholder="{$ 'An external (HTTP) URL to load the image from'|translate $}">
|
||||
<p class="help-block alert alert-danger"
|
||||
ng-show="imageForm.image_url.$invalid && imageForm.image_url.$dirty">
|
||||
<translate>An external (HTTP) URL is required</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6" ng-if="ctrl.image.source_type === 'url'">
|
||||
<div class="form-group">
|
||||
<label class="control-label required">
|
||||
<translate>Copy Data</translate>
|
||||
</label>
|
||||
<div class="form-field">
|
||||
<div class="btn-group">
|
||||
<label class="btn btn-toggle"
|
||||
ng-repeat="option in ctrl.imageCopyOptions"
|
||||
ng-model="ctrl.image.is_copying"
|
||||
btn-radio="option.value">{$ ::option.label $}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-container_format" translate>Format</label><span class="hz-icon-required fa fa-asterisk"></span>
|
||||
<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 class="section-title" translate>Image Requirements</h3>
|
||||
<div class="subtitle"></div>
|
||||
|
||||
<div class="selected-source">
|
||||
<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</translate>
|
||||
</label>
|
||||
<select class="form-control" id="imageForm-kernel" name="kernel" ng-model="ctrl.image.kernel">
|
||||
<option value="" selected="selected" translate>Choose an image</option>
|
||||
<option ng-repeat="kernel in ctrl.kernelImages" value="{$ kernel.id $}">{$ kernel.name $}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="imageForm-ramdisk">
|
||||
<translate>Ramdisk</translate>
|
||||
</label>
|
||||
<select class="form-control" id="imageForm-ramdisk" name="ramdisk" ng-model="ctrl.image.ramdisk">
|
||||
<option value="" selected="selected" translate>Choose an image</option>
|
||||
<option ng-repeat="ramdisk in ctrl.ramdiskImages" value="{$ ramdisk.id $}">{$ ramdisk.name $}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="imageForm-architecture">
|
||||
<translate>Architecture</translate>
|
||||
</label>
|
||||
<input id="imageForm-architecture" name="architecture"
|
||||
type="text" class="form-control"
|
||||
ng-model="ctrl.image.architecture"
|
||||
ng-maxlength="ctrl.validationRules.fieldMaxLength">
|
||||
</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)</translate>
|
||||
</label>
|
||||
<input id="imageForm-min_disk" name="min_disk"
|
||||
type="number" class="form-control"
|
||||
ng-required="true"
|
||||
ng-pattern="ctrl.validationRules.integer"
|
||||
ng-model="ctrl.image.min_disk"
|
||||
min=0>
|
||||
</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)</translate>
|
||||
</label>
|
||||
<input id="imageForm-min_ram" name="min_ram"
|
||||
type="number" class="form-control"
|
||||
ng-required="true"
|
||||
ng-pattern="ctrl.validationRules.integer" ng-model="ctrl.image.min_ram"
|
||||
min=0>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title" translate>Image Sharing</h3>
|
||||
<div class="subtitle"></div>
|
||||
|
||||
<div class="selected-source">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label required">
|
||||
<translate>Visibility</translate>
|
||||
</label>
|
||||
<div class="form-field">
|
||||
<div class="btn-group">
|
||||
<label class="btn btn-toggle"
|
||||
ng-repeat="option in ctrl.imageVisibilityOptions"
|
||||
ng-model="ctrl.image.visibility"
|
||||
btn-radio="option.value">{$ ::option.label $}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label required">
|
||||
<translate>Protected</translate>
|
||||
</label>
|
||||
<div class="form-field">
|
||||
<div class="btn-group">
|
||||
<label class="btn btn-toggle"
|
||||
ng-repeat="option in ctrl.imageProtectedOptions"
|
||||
ng-model="ctrl.image.protected"
|
||||
btn-radio="option.value">{$ ::option.label $}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,4 +1,5 @@
|
|||
/**
|
||||
* (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
|
||||
|
@ -61,12 +62,6 @@
|
|||
|
||||
$scope.imagePromise.then(init);
|
||||
|
||||
var imageChangedWatcher;
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
imageChangedWatcher();
|
||||
});
|
||||
|
||||
///////////////////////////
|
||||
|
||||
function getConfiguredFormats(response) {
|
||||
|
@ -93,14 +88,6 @@
|
|||
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() {
|
||||
|
|
|
@ -134,20 +134,6 @@
|
|||
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();
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
/*
|
||||
* 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
|
||||
/**
|
||||
* (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.
|
||||
* 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';
|
||||
|
@ -47,7 +49,15 @@
|
|||
$scope.$watchCollection(getTree, onMetadataChanged);
|
||||
/* eslint-enable angular/ng_controller_as */
|
||||
|
||||
$scope.imagePromise.then(init);
|
||||
if ($scope.imagePromise) {
|
||||
// Launched from an image.
|
||||
$scope.imagePromise.then(init);
|
||||
} else {
|
||||
$q.all({
|
||||
available: metadataService.getNamespaces('image'),
|
||||
existing: getExistingMetdataPromise({})
|
||||
}).then(onMetadataGet);
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
|
@ -55,7 +65,7 @@
|
|||
var image = response.data;
|
||||
$q.all({
|
||||
available: metadataService.getNamespaces('image'),
|
||||
existing: metadataService.getMetadata('image', image.id)
|
||||
existing: getExistingMetdataPromise(image)
|
||||
}).then(onMetadataGet);
|
||||
}
|
||||
|
||||
|
@ -70,6 +80,16 @@
|
|||
return ctrl.tree.getExisting();
|
||||
}
|
||||
|
||||
function getExistingMetdataPromise(image) {
|
||||
if (angular.isDefined(image.id)) {
|
||||
return metadataService.getMetadata('image', image.id);
|
||||
} else {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({data: []});
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
||||
|
||||
function onMetadataChanged(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
$scope.$emit(events.IMAGE_METADATA_CHANGED, newValue);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/*
|
||||
/**
|
||||
* (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
|
||||
|
@ -87,6 +88,36 @@
|
|||
expect(metadataTreeService.Tree).toHaveBeenCalledWith(availableMetadata, existingMetadata);
|
||||
});
|
||||
|
||||
it('should setup up the metadata tree even without an image', function() {
|
||||
expect($scope.imagePromise).toBeUndefined();
|
||||
|
||||
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(availableMetadata, []);
|
||||
});
|
||||
|
||||
it('should setup up the metadata tree if image does not exist', function() {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({data: {}});
|
||||
$scope.imagePromise = deferred.promise;
|
||||
|
||||
spyOn(metadataTreeService, 'Tree').and.returnValue(mockTree);
|
||||
spyOn(metadataService, 'getNamespaces').and.callThrough();
|
||||
|
||||
var ctrl = createController();
|
||||
$scope.$apply();
|
||||
|
||||
expect(ctrl.tree).toEqual(mockTree);
|
||||
expect(metadataTreeService.Tree).toHaveBeenCalledWith([], []);
|
||||
expect(metadataTreeService.Tree).toHaveBeenCalledWith(availableMetadata, []);
|
||||
});
|
||||
|
||||
it('should emit imageMetadataChanged event when metadata changes', function() {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({data: {id: '1'}});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div>
|
||||
<h1 translate>Metadata Help</h1>
|
||||
<h3 translate>Metadata Help</h3>
|
||||
<p translate>You can add arbitrary metadata to your image.</p>
|
||||
<p translate>
|
||||
Metadata is used to provide additional information about the
|
||||
|
|
|
@ -1,6 +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>
|
||||
<metadata-tree model="metadataCtrl.tree" form="updateMetadataForm"></metadata-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue