Add Action to delete multiple and single images to images panel
Adds the ability to delete multiple images from the images panel. Adding row action to delete single image to angular images panel Work needs to be done on the actionss directive per the feedback To test set DISABLED = False in _1051_project_ng_images_panel.py Co-Authored-By: Kristine Brown<kbrown@thoughtworks.com> Co-Authored-By: Errol Pais<epais@thoughtworks.com> Co-Authored-By: Kyle Olivo<keolivo@thoughtworks.com> Change-Id: I59d13e0b2225f2b3a05f93b2356029561dbd28bc Partially-Implements: blueprint angularize-images-table
This commit is contained in:
parent
7a1e069af4
commit
2b3ab06715
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.factory('horizon.app.core.images.actions.batchDeleteService', batchDeleteService);
|
||||
|
||||
batchDeleteService.$inject = [
|
||||
'horizon.app.core.images.actions.deleteImageService',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.app.core.images.actions.batchDeleteService
|
||||
*
|
||||
* @Description
|
||||
* Brings up the delete images confirmation modal dialog.
|
||||
* On submit, delete selected images.
|
||||
* On cancel, do nothing.
|
||||
*/
|
||||
function batchDeleteService(
|
||||
deleteImageService,
|
||||
policy,
|
||||
gettext
|
||||
) {
|
||||
var context = {
|
||||
title: gettext('Confirm Delete Images'),
|
||||
/* eslint-disable max-len */
|
||||
message: gettext('You have selected "%s". Please confirm your selection. Deleted images are not recoverable.'),
|
||||
/* eslint-enable max-len */
|
||||
submit: gettext('Delete Images'),
|
||||
success: gettext('Deleted Images: %s.'),
|
||||
error: gettext('Unable to delete Images: %s.')
|
||||
};
|
||||
|
||||
var service = {
|
||||
initScope: initScope,
|
||||
perform: perform,
|
||||
allowed: allowed
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
// include this function in your service
|
||||
// if you plan to emit events to the parent controller
|
||||
function initScope(newScope) {
|
||||
deleteImageService.initScope(newScope, context);
|
||||
}
|
||||
|
||||
// delete selected image objects
|
||||
function perform(selected) {
|
||||
deleteImageService.perform(getSelectedImages(selected));
|
||||
}
|
||||
|
||||
function allowed() {
|
||||
return policy.ifAllowed({ rules: [['image', 'delete_image']] });
|
||||
}
|
||||
|
||||
function getSelectedImages(selected) {
|
||||
return Object.keys(selected).filter(isChecked).map(getImage);
|
||||
|
||||
function isChecked(value) {
|
||||
return selected[value].checked;
|
||||
}
|
||||
|
||||
function getImage(value) {
|
||||
return selected[value].item;
|
||||
}
|
||||
}
|
||||
|
||||
} // end of batchDeleteService
|
||||
})(); // end of IIFE
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* 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.batchDeleteService', function() {
|
||||
|
||||
var deleteImageService = {
|
||||
initScope: function() {},
|
||||
perform: function () {}
|
||||
};
|
||||
|
||||
var policyAPI = {
|
||||
ifAllowed: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({allowed: true});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var service, $scope;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.openstack-service-api', function($provide) {
|
||||
$provide.value('horizon.app.core.openstack-service-api.policy', policyAPI);
|
||||
}));
|
||||
beforeEach(module('horizon.app.core.images', function($provide) {
|
||||
$provide.value('horizon.app.core.images.actions.deleteImageService', deleteImageService);
|
||||
}));
|
||||
beforeEach(inject(function($injector, _$rootScope_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
service = $injector.get('horizon.app.core.images.actions.batchDeleteService');
|
||||
}));
|
||||
|
||||
it('should init the deleteImageService', function() {
|
||||
spyOn(deleteImageService, 'initScope').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
|
||||
expect(deleteImageService.initScope).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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', 'delete_image']] });
|
||||
});
|
||||
|
||||
it('pass the image to the deleteImageService', function() {
|
||||
spyOn(deleteImageService, 'perform').and.callThrough();
|
||||
|
||||
var selected = {
|
||||
image1: {checked: true, item: {name: 'image1', id: '1'}},
|
||||
image2: {checked: true, item: {name: 'image2', id: '2'}}
|
||||
};
|
||||
|
||||
service.perform(selected);
|
||||
|
||||
expect(deleteImageService.perform).toHaveBeenCalledWith(
|
||||
[{id: '1', name: 'image1'}, {id: '2', name: 'image2'}]
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.factory('horizon.app.core.images.actions.deleteImageService', deleteImageService);
|
||||
|
||||
deleteImageService.$inject = [
|
||||
'$q',
|
||||
'horizon.app.core.openstack-service-api.glance',
|
||||
'horizon.app.core.openstack-service-api.keystone',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.framework.util.i18n.gettext',
|
||||
'horizon.framework.util.q.extensions',
|
||||
'horizon.framework.widgets.modal.deleteModalService',
|
||||
'horizon.framework.widgets.toast.service',
|
||||
'horizon.app.core.images.events'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.app.core.images.actions.deleteImageService
|
||||
*
|
||||
* @Description
|
||||
* Brings up the delete images confirmation modal dialog.
|
||||
|
||||
* On submit, delete given images.
|
||||
* On cancel, do nothing.
|
||||
*/
|
||||
function deleteImageService(
|
||||
$q,
|
||||
glance,
|
||||
keystone,
|
||||
policy,
|
||||
gettext,
|
||||
$qExtensions,
|
||||
deleteModal,
|
||||
toast,
|
||||
events
|
||||
) {
|
||||
var scope, context;
|
||||
var notAllowedMessage = gettext("You are not allowed to delete images: %s");
|
||||
var deleteImagePromise = policy.ifAllowed({rules: [['image', 'delete_image']]});
|
||||
var userSessionPromise = createUserSessionPromise();
|
||||
|
||||
var service = {
|
||||
initScope: initScope,
|
||||
allowed: allowed,
|
||||
perform: perform
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
function initScope(newScope, actionContext) {
|
||||
scope = newScope;
|
||||
context = {
|
||||
labels: actionContext,
|
||||
successEvent: events.DELETE_SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
function perform(images) {
|
||||
context.deleteEntity = deleteImage;
|
||||
$qExtensions.allSettled(images.map(checkPermission)).then(afterCheck);
|
||||
}
|
||||
|
||||
function allowed(image) {
|
||||
return $q.all([
|
||||
notProtected(image),
|
||||
deleteImagePromise,
|
||||
ownedByUser(image),
|
||||
notDeleted(image)
|
||||
]);
|
||||
}
|
||||
|
||||
function checkPermission(image) {
|
||||
return {promise: allowed(image), context: image};
|
||||
}
|
||||
|
||||
function afterCheck(result) {
|
||||
if (result.fail.length > 0) {
|
||||
toast.add('error', getMessage(notAllowedMessage, result.fail));
|
||||
}
|
||||
if (result.pass.length > 0) {
|
||||
deleteModal.open(scope, result.pass.map(getEntity), context);
|
||||
}
|
||||
}
|
||||
|
||||
function createUserSessionPromise() {
|
||||
var deferred = $q.defer();
|
||||
keystone.getCurrentUserSession().success(onUserSessionGet);
|
||||
return deferred.promise;
|
||||
|
||||
function onUserSessionGet(userSession) {
|
||||
deferred.resolve(userSession);
|
||||
}
|
||||
}
|
||||
|
||||
function ownedByUser(image) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
userSessionPromise.then(onUserSessionGet);
|
||||
|
||||
return deferred.promise;
|
||||
|
||||
function onUserSessionGet(userSession) {
|
||||
if (userSession.project_id === image.owner) {
|
||||
deferred.resolve();
|
||||
} else {
|
||||
deferred.reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notDeleted(image) {
|
||||
return $qExtensions.booleanAsPromise(image.status !== 'deleted');
|
||||
}
|
||||
|
||||
function notProtected(image) {
|
||||
return $qExtensions.booleanAsPromise(!image.protected);
|
||||
}
|
||||
|
||||
function deleteImage(image) {
|
||||
return glance.deleteImage(image, true);
|
||||
}
|
||||
|
||||
function getMessage(message, entities) {
|
||||
return interpolate(message, [entities.map(getName).join(", ")]);
|
||||
}
|
||||
|
||||
function getName(result) {
|
||||
return getEntity(result).name;
|
||||
}
|
||||
|
||||
function getEntity(result) {
|
||||
return result.context;
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,256 @@
|
|||
/**
|
||||
* 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.deleteImageService', function() {
|
||||
|
||||
var context = {
|
||||
title: gettext('Confirm Delete Images'),
|
||||
message: gettext('selected "%s"'),
|
||||
submit: gettext('Delete'),
|
||||
success: gettext('Deleted : %s.'),
|
||||
error: gettext('Unable to delete: %s.')
|
||||
};
|
||||
|
||||
var deleteModalService = {
|
||||
open: function () {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
var glanceAPI = {
|
||||
deleteImage: function() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
var policyAPI = {
|
||||
ifAllowed: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({allowed: true});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var keyStoneAPI = {
|
||||
getCurrentUserSession: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({project_id: 'project'});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var service, $scope;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
beforeEach(module('horizon.framework.widgets.modal', function($provide) {
|
||||
$provide.value('horizon.framework.widgets.modal.deleteModalService', deleteModalService);
|
||||
}));
|
||||
|
||||
beforeEach(module('horizon.app.core.openstack-service-api', function($provide) {
|
||||
$provide.value('horizon.app.core.openstack-service-api.glance', glanceAPI);
|
||||
spyOn(policyAPI, 'ifAllowed').and.callThrough();
|
||||
$provide.value('horizon.app.core.openstack-service-api.policy', policyAPI);
|
||||
spyOn(keyStoneAPI, 'getCurrentUserSession').and.callThrough();
|
||||
$provide.value('horizon.app.core.openstack-service-api.keystone', keyStoneAPI);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector, _$rootScope_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
service = $injector.get('horizon.app.core.images.actions.deleteImageService');
|
||||
}));
|
||||
|
||||
it('should open the delete modal with correct messages', function() {
|
||||
var images = [
|
||||
{protected: false, owner: 'project', status: 'active', name: 'image1', id: '1'}
|
||||
];
|
||||
|
||||
spyOn(deleteModalService, 'open');
|
||||
|
||||
service.initScope($scope, context);
|
||||
service.perform(images);
|
||||
$scope.$apply();
|
||||
|
||||
expect(deleteModalService.open).toHaveBeenCalled();
|
||||
|
||||
var args = deleteModalService.open.calls.argsFor(0);
|
||||
var labels = args[2].labels;
|
||||
|
||||
expect(labels.title).toEqual('Confirm Delete Images');
|
||||
expect(labels.message).toEqual('selected "%s"');
|
||||
expect(labels.submit).toEqual('Delete');
|
||||
expect(labels.success).toEqual('Deleted : %s.');
|
||||
expect(labels.error).toEqual('Unable to delete: %s.');
|
||||
});
|
||||
|
||||
it('should pass in the success and error events to be thrown', function() {
|
||||
var images = [
|
||||
{protected: false, owner: 'project', status: 'active', name: 'image1', id: '1'}
|
||||
];
|
||||
|
||||
spyOn(deleteModalService, 'open');
|
||||
|
||||
service.initScope($scope, context);
|
||||
service.perform(images);
|
||||
$scope.$apply();
|
||||
|
||||
expect(deleteModalService.open).toHaveBeenCalled();
|
||||
|
||||
var args = deleteModalService.open.calls.argsFor(0);
|
||||
var contextArg = args[2];
|
||||
|
||||
expect(contextArg.successEvent).toEqual('horizon.app.core.images.DELETE_SUCCESS');
|
||||
});
|
||||
|
||||
it('should open the delete modal with correct entities', function() {
|
||||
var images = [
|
||||
{protected: false, owner: 'project', status: 'active', name: 'image1', id: '1'},
|
||||
{protected: false, owner: 'project', status: 'active', name: 'image2', id: '2'}
|
||||
];
|
||||
|
||||
spyOn(deleteModalService, 'open');
|
||||
|
||||
service.initScope($scope, context);
|
||||
service.perform(images);
|
||||
$scope.$apply();
|
||||
|
||||
expect(deleteModalService.open).toHaveBeenCalled();
|
||||
|
||||
var args = deleteModalService.open.calls.argsFor(0);
|
||||
var entities = args[1];
|
||||
|
||||
expect(entities[0].id).toEqual('1');
|
||||
expect(entities[0].name).toEqual('image1');
|
||||
expect(entities[1].id).toEqual('2');
|
||||
expect(entities[1].name).toEqual('image2');
|
||||
});
|
||||
|
||||
it('should only attempt to delete images that are allowed to be deleted', function() {
|
||||
var images = [
|
||||
{protected: false, owner: 'project', status: 'active', name: 'image1', id: '1'},
|
||||
{protected: false, owner: 'project', status: 'active', name: 'image2', id: '2'},
|
||||
{protected: false, owner: 'project', status: 'deleted', name: 'image3', id: '3'},
|
||||
{protected: false, owner: 'project1', status: 'active', name: 'image4', id: '4'},
|
||||
{protected: true, owner: 'project', status: 'active', name: 'image5', id: '5'}
|
||||
];
|
||||
|
||||
spyOn(deleteModalService, 'open');
|
||||
|
||||
service.initScope($scope, context);
|
||||
service.perform(images);
|
||||
$scope.$apply();
|
||||
|
||||
expect(deleteModalService.open).toHaveBeenCalled();
|
||||
|
||||
var args = deleteModalService.open.calls.argsFor(0);
|
||||
var entities = args[1];
|
||||
|
||||
expect(entities[0].id).toEqual('1');
|
||||
expect(entities[0].name).toEqual('image1');
|
||||
expect(entities[1].id).toEqual('2');
|
||||
expect(entities[1].name).toEqual('image2');
|
||||
});
|
||||
|
||||
it('should not open modal if no images can be deleted', function() {
|
||||
var images = [
|
||||
{protected: false, owner: 'project', status: 'deleted', name: 'image3', id: '3'},
|
||||
{protected: false, owner: 'project1', status: 'active', name: 'image4', id: '4'},
|
||||
{protected: true, owner: 'project', status: 'active', name: 'image5', id: '5'}
|
||||
];
|
||||
|
||||
spyOn(deleteModalService, 'open');
|
||||
|
||||
service.initScope($scope, context);
|
||||
service.perform(images);
|
||||
$scope.$apply();
|
||||
|
||||
expect(deleteModalService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass in a function that deletes an image', function() {
|
||||
var image = {protected: false, owner: 'project', status: 'active', name: 'image1', id: '1'};
|
||||
|
||||
spyOn(deleteModalService, 'open');
|
||||
spyOn(glanceAPI, 'deleteImage');
|
||||
|
||||
service.initScope($scope, context);
|
||||
service.perform([image]);
|
||||
$scope.$apply();
|
||||
|
||||
var contextArg = deleteModalService.open.calls.argsFor(0)[2];
|
||||
var deleteFunction = contextArg.deleteEntity;
|
||||
|
||||
deleteFunction(image.id);
|
||||
|
||||
expect(glanceAPI.deleteImage).toHaveBeenCalledWith(image.id, true);
|
||||
});
|
||||
|
||||
it('should allow delete if image can be deleted', function() {
|
||||
var image = {protected: false, owner: 'project', status: 'active'};
|
||||
permissionShouldPass(service.allowed(image));
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
it('should not allow delete if image is protected', function() {
|
||||
var image = {protected: true, owner: 'project', status: 'active'};
|
||||
permissionShouldFail(service.allowed(image));
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
it('should not allow delete if image is not owned by user', function() {
|
||||
var image = {protected: false, owner: 'another_project', status: 'active'};
|
||||
permissionShouldFail(service.allowed(image));
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
it('should not allow delete if image status is deleted', function() {
|
||||
var image = {protected: false, owner: 'project', status: 'deleted'};
|
||||
permissionShouldFail(service.allowed(image));
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
function permissionShouldPass(permissions) {
|
||||
permissions.then(
|
||||
function() {
|
||||
expect(true).toBe(true);
|
||||
},
|
||||
function() {
|
||||
expect(false).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
function permissionShouldFail(permissions) {
|
||||
permissions.then(
|
||||
function() {
|
||||
expect(false).toBe(true);
|
||||
},
|
||||
function() {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})();
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.images')
|
||||
.factory('horizon.app.core.images.actions.deleteService', deleteService);
|
||||
|
||||
deleteService.$inject = [
|
||||
'horizon.app.core.images.actions.deleteImageService',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.app.core.images.actions.deleteService
|
||||
*
|
||||
* @Description
|
||||
* Brings up the delete image confirmation modal dialog.
|
||||
* On submit, delete selected image.
|
||||
* On cancel, do nothing.
|
||||
*/
|
||||
function deleteService(deleteImageService, gettext) {
|
||||
|
||||
var context = {
|
||||
title: gettext('Confirm Delete Image'),
|
||||
/* eslint-disable max-len */
|
||||
message: gettext('You have selected "%s". Please confirm your selection. Deleted images are not recoverable.'),
|
||||
/* eslint-enable max-len */
|
||||
submit: gettext('Delete Image'),
|
||||
success: gettext('Deleted Image: %s.'),
|
||||
error: gettext('Unable to delete Image: %s.')
|
||||
};
|
||||
|
||||
var service = {
|
||||
initScope: initScope,
|
||||
perform: perform,
|
||||
allowed: deleteImageService.allowed
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
// include this function in your service
|
||||
// if you plan to emit events to the parent controller
|
||||
function initScope(newScope) {
|
||||
deleteImageService.initScope(newScope, context);
|
||||
}
|
||||
|
||||
// delete selected image
|
||||
function perform(image) {
|
||||
deleteImageService.perform([image]);
|
||||
}
|
||||
|
||||
} // end of deleteService
|
||||
})(); // end of IIFE
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* 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.deleteService', function() {
|
||||
|
||||
var deleteImageService = {
|
||||
initScope: function() {},
|
||||
perform: function () {}
|
||||
};
|
||||
|
||||
var service, $scope;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework.util'));
|
||||
beforeEach(module('horizon.framework.util.http'));
|
||||
|
||||
beforeEach(module('horizon.framework.widgets'));
|
||||
beforeEach(module('horizon.framework.widgets.toast'));
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
|
||||
beforeEach(module('horizon.app.core.images', function($provide) {
|
||||
$provide.value('horizon.app.core.images.actions.deleteImageService', deleteImageService);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector, _$rootScope_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
service = $injector.get('horizon.app.core.images.actions.deleteService');
|
||||
}));
|
||||
|
||||
it('should init the deleteImageService', function() {
|
||||
spyOn(deleteImageService, 'initScope').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
|
||||
expect(deleteImageService.initScope).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('pass the image to the deleteImageService', function() {
|
||||
spyOn(deleteImageService, 'perform').and.callThrough();
|
||||
var image = {id: '1', name: 'name', extra: 'extra'};
|
||||
service.perform(image);
|
||||
|
||||
expect(deleteImageService.perform).toHaveBeenCalledWith([image]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
|
@ -27,6 +27,7 @@
|
|||
*/
|
||||
angular
|
||||
.module('horizon.app.core.images', [])
|
||||
.constant('horizon.app.core.images.events', events())
|
||||
.config(config);
|
||||
|
||||
config.$inject = [
|
||||
|
@ -34,6 +35,17 @@
|
|||
'$windowProvider'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc value
|
||||
* @name horizon.app.core.images.events
|
||||
* @description a list of events for images
|
||||
*/
|
||||
function events() {
|
||||
return {
|
||||
DELETE_SUCCESS: 'horizon.app.core.images.DELETE_SUCCESS'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.images.basePath
|
||||
* @description Base path for the images code
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* 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.batch-actions.service', tableBatchActions);
|
||||
|
||||
tableBatchActions.$inject = [
|
||||
'horizon.app.core.images.actions.batchDeleteService',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc factory
|
||||
* @name horizon.app.core.images.table.batch-actions.service
|
||||
* @description A list of table batch actions.
|
||||
*/
|
||||
function tableBatchActions(
|
||||
deleteService,
|
||||
gettext
|
||||
) {
|
||||
var service = {
|
||||
initScope: initScope,
|
||||
actions: actions
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
///////////////
|
||||
|
||||
function initScope(scope) {
|
||||
angular.forEach([deleteService], setActionScope);
|
||||
|
||||
function setActionScope(action) {
|
||||
action.initScope(scope.$new());
|
||||
}
|
||||
}
|
||||
|
||||
function actions() {
|
||||
return [{
|
||||
service: deleteService,
|
||||
template: {
|
||||
type: 'delete-selected',
|
||||
text: gettext('Delete Images')
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 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.batch-actions.service', function() {
|
||||
var service;
|
||||
|
||||
var batchDeleteService = {
|
||||
initScope: angular.noop
|
||||
};
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
beforeEach(module('horizon.app.core.images', function($provide) {
|
||||
$provide.value('horizon.app.core.images.actions.batchDeleteService', batchDeleteService);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function ($injector) {
|
||||
service = $injector.get('horizon.app.core.images.batch-actions.service');
|
||||
}));
|
||||
|
||||
it('should call initScope on batchDeleteService', function() {
|
||||
spyOn(batchDeleteService, 'initScope');
|
||||
var scope = {$new: function() { return 'custom_scope'; }};
|
||||
service.initScope(scope);
|
||||
|
||||
expect(batchDeleteService.initScope).toHaveBeenCalledWith('custom_scope');
|
||||
});
|
||||
|
||||
it('should return delete action', function() {
|
||||
var actions = service.actions();
|
||||
|
||||
expect(actions.length).toEqual(1);
|
||||
expect(actions[0].service).toEqual(batchDeleteService);
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -30,25 +30,42 @@
|
|||
.controller('imagesTableController', ImagesTableController);
|
||||
|
||||
ImagesTableController.$inject = [
|
||||
'horizon.app.core.images.basePath',
|
||||
'$scope',
|
||||
'horizon.app.core.images.batch-actions.service',
|
||||
'horizon.app.core.images.row-actions.service',
|
||||
'horizon.app.core.images.events',
|
||||
'horizon.app.core.openstack-service-api.glance'
|
||||
];
|
||||
|
||||
function ImagesTableController(basepath, glance) {
|
||||
|
||||
function ImagesTableController(
|
||||
$scope,
|
||||
batchActions,
|
||||
rowActions,
|
||||
events,
|
||||
glance
|
||||
) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.checked = {};
|
||||
|
||||
ctrl.images = [];
|
||||
ctrl.imagesSrc = [];
|
||||
ctrl.checked = {};
|
||||
ctrl.path = basepath + 'table/';
|
||||
|
||||
ctrl.batchActions = batchActions;
|
||||
ctrl.batchActions.initScope($scope);
|
||||
|
||||
ctrl.rowActions = rowActions;
|
||||
ctrl.rowActions.initScope($scope);
|
||||
|
||||
var deleteWatcher = $scope.$on(events.DELETE_SUCCESS, onDeleteSuccess);
|
||||
|
||||
$scope.$on('$destroy', destroy);
|
||||
|
||||
init();
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
function init() {
|
||||
// if user has permission
|
||||
// fetch table data and populate it
|
||||
glance.getImages().success(onGetImages);
|
||||
}
|
||||
|
||||
|
@ -56,6 +73,31 @@
|
|||
ctrl.imagesSrc = response.items;
|
||||
}
|
||||
|
||||
function onDeleteSuccess(e, removedImageIds) {
|
||||
ctrl.imagesSrc = difference(ctrl.imagesSrc, removedImageIds, 'id');
|
||||
|
||||
/* eslint-disable angular/ng_controller_as */
|
||||
$scope.selected = {};
|
||||
$scope.numSelected = 0;
|
||||
/* eslint-enable angular/ng_controller_as */
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function difference(currentList, otherList, key) {
|
||||
return currentList.filter(filter);
|
||||
|
||||
function filter(elem) {
|
||||
return otherList.filter(function filterDeletedItem(deletedItem) {
|
||||
return deletedItem === elem[key];
|
||||
}).length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
deleteWatcher();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
@ -19,50 +19,72 @@
|
|||
|
||||
describe('horizon.app.core.images table controller', function() {
|
||||
|
||||
function fakeGlance() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({
|
||||
items : []
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
var glanceAPI = {
|
||||
getImages: function() {
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback({items : [{id: '1'},{id: '2'}]});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var controller, glanceAPI, staticUrl;
|
||||
var $scope, controller, events;
|
||||
|
||||
///////////////////////
|
||||
beforeEach(module('ui.bootstrap'));
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
beforeEach(module('horizon.framework.conf'));
|
||||
beforeEach(module('horizon.framework.util.http'));
|
||||
beforeEach(module('horizon.framework.widgets.toast'));
|
||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||
beforeEach(module('horizon.app.core.openstack-service-api', function($provide) {
|
||||
$provide.value('horizon.app.core.openstack-service-api.glance', glanceAPI);
|
||||
}));
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.app.core.images'));
|
||||
beforeEach(inject(function($injector) {
|
||||
|
||||
glanceAPI = $injector.get('horizon.app.core.openstack-service-api.glance');
|
||||
beforeEach(inject(function ($injector, _$rootScope_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
|
||||
events = $injector.get('horizon.app.core.images.events');
|
||||
controller = $injector.get('$controller');
|
||||
staticUrl = $injector.get('$window').STATIC_URL;
|
||||
|
||||
spyOn(glanceAPI, 'getImages').and.callFake(fakeGlance);
|
||||
spyOn(glanceAPI, 'getImages').and.callThrough();
|
||||
}));
|
||||
|
||||
function createController() {
|
||||
return controller('imagesTableController', {
|
||||
glanceAPI: glanceAPI
|
||||
glanceAPI: glanceAPI,
|
||||
$scope: $scope
|
||||
});
|
||||
}
|
||||
|
||||
it('should set path properly', function() {
|
||||
var path = staticUrl + 'app/core/images/table/';
|
||||
expect(createController().path).toEqual(path);
|
||||
it('should invoke glance apis', function() {
|
||||
var ctrl = createController();
|
||||
|
||||
expect(glanceAPI.getImages).toHaveBeenCalled();
|
||||
expect(ctrl.imagesSrc).toEqual([{id: '1'}, {id: '2'}]);
|
||||
});
|
||||
|
||||
it('should invoke glance apis', function() {
|
||||
createController();
|
||||
expect(glanceAPI.getImages).toHaveBeenCalled();
|
||||
it('should refresh images after delete', function() {
|
||||
var ctrl = createController();
|
||||
expect(ctrl.imagesSrc).toEqual([{id: '1'}, {id: '2'}]);
|
||||
|
||||
$scope.$emit(events.DELETE_SUCCESS, ['1']);
|
||||
|
||||
expect($scope.selected).toEqual({});
|
||||
expect($scope.numSelected).toEqual(0);
|
||||
expect(ctrl.imagesSrc).toEqual([{id: '2'}]);
|
||||
});
|
||||
|
||||
it('should destroy the event watchers', function() {
|
||||
var ctrl = createController();
|
||||
|
||||
$scope.$emit('$destroy');
|
||||
$scope.$emit(events.DELETE_SUCCESS, ['1']);
|
||||
|
||||
expect(ctrl.imagesSrc).toEqual([{id: '1'}, {id: '2'}]);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
-->
|
||||
<th colspan="100" class="search-header">
|
||||
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
|
||||
<actions allowed="table.batchActions.actions" type="batch">
|
||||
</actions>
|
||||
</hz-search-bar>
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -53,8 +55,7 @@
|
|||
Include action-col if you want to perform actions.
|
||||
rsp-p1 rsp-p2 are responsive priority as user resizes window.
|
||||
-->
|
||||
<tr ng-repeat-start="image in table.images track by image.id"
|
||||
ng-class="{'st-selected': checked[image.id]}">
|
||||
<tr ng-repeat-start="image in table.images track by image.id">
|
||||
|
||||
<td class="select-col">
|
||||
<input type="checkbox"
|
||||
|
@ -74,6 +75,15 @@
|
|||
<td class="rsp-p2">{$ image.protected | yesno $}</td>
|
||||
<td class="rsp-p2">{$ image.disk_format | noValue | uppercase $}</td>
|
||||
<td class="rsp-p2">{$ image.size | bytes $}</td>
|
||||
|
||||
<td class="action-col">
|
||||
<!--
|
||||
Table-row-action-column:
|
||||
Actions taken here applies to a single item/row.
|
||||
-->
|
||||
<actions allowed="table.rowActions.actions" type="row" item="image">
|
||||
</actions>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat-end class="detail-row">
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* 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.row-actions.service', rowActions);
|
||||
|
||||
rowActions.$inject = [
|
||||
'horizon.app.core.images.actions.deleteService',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc factory
|
||||
* @name horizon.app.core.images.table.row-actions.service
|
||||
* @description A list of row actions.
|
||||
*/
|
||||
function rowActions(
|
||||
deleteService,
|
||||
gettext
|
||||
) {
|
||||
var service = {
|
||||
initScope: initScope,
|
||||
actions: actions
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
///////////////
|
||||
|
||||
function initScope(scope) {
|
||||
angular.forEach([deleteService], setActionScope);
|
||||
|
||||
function setActionScope(action) {
|
||||
action.initScope(scope.$new());
|
||||
}
|
||||
}
|
||||
|
||||
function actions() {
|
||||
return [{
|
||||
service: deleteService,
|
||||
template: {
|
||||
text: gettext('Delete Image'),
|
||||
type: 'delete'
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* 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.row-actions.service', function() {
|
||||
var service;
|
||||
var deleteService = {
|
||||
initScope: angular.noop
|
||||
};
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
beforeEach(module('horizon.app.core.images', function($provide) {
|
||||
$provide.value('horizon.app.core.images.actions.deleteService', deleteService);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function ($injector) {
|
||||
service = $injector.get('horizon.app.core.images.row-actions.service');
|
||||
}));
|
||||
|
||||
it('should call initScope on deleteService', function() {
|
||||
spyOn(deleteService, 'initScope');
|
||||
var scope = {$new: function() { return 'custom_scope'; }};
|
||||
service.initScope(scope);
|
||||
|
||||
expect(deleteService.initScope).toHaveBeenCalledWith('custom_scope');
|
||||
});
|
||||
|
||||
it('should return delete action', function() {
|
||||
var actions = service.actions();
|
||||
|
||||
expect(actions.length).toEqual(1);
|
||||
expect(actions[0].service).toEqual(deleteService);
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -197,7 +197,7 @@
|
|||
*
|
||||
*/
|
||||
function deleteImage(imageId, suppressError) {
|
||||
var promise = apiService.delete('/api/glance/images/' + imageId);
|
||||
var promise = apiService.delete('/api/glance/images/' + imageId + '/');
|
||||
|
||||
return suppressError ? promise : promise.error(function() {
|
||||
toastService.add('error', gettext('Unable to delete the image with id: ') + imageId);
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
{
|
||||
"func": "deleteImage",
|
||||
"method": "delete",
|
||||
"path": "/api/glance/images/42",
|
||||
"path": "/api/glance/images/42/",
|
||||
"error": "Unable to delete the image with id: 42",
|
||||
"testInput": [
|
||||
42
|
||||
|
|
Loading…
Reference in New Issue