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:
Rajat Vig 2015-10-09 23:24:57 -07:00
parent 7a1e069af4
commit 2b3ab06715
16 changed files with 1072 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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