Using registration for auto-generation of Image feature

This patch removes the hard-coded table and uses instead the auto-table
feature.

Change-Id: I334b70fe9e4c6ef166b41c0e41f27e105e9af682
This commit is contained in:
Matt Borland 2016-04-22 13:22:11 -06:00
parent 30e37aaf2c
commit 7e6ca1a6cc
13 changed files with 275 additions and 663 deletions

View File

@ -40,7 +40,7 @@
detailsTemplateUrl: ctrl.resourceType.summaryTemplateUrl,
selectAll: true,
expand: true,
trackId: 'id',
trackId: ctrl.trackBy || 'id',
searchColumnSpan: 7,
actionColumnSpan: 5,
columns: ctrl.resourceType.getTableColumns()

View File

@ -47,7 +47,8 @@
var directive = {
restrict: 'E',
scope: {
resourceTypeName: '@'
resourceTypeName: '@',
trackBy: '@?'
},
bindToController: true,
templateUrl: basePath + 'table/hz-resource-table.html',

View File

@ -117,6 +117,7 @@
////////////
it('should open the delete modal and show correct labels', testSingleLabels);
it('should open the delete modal and show correct labels, one object', testSingleObject);
it('should open the delete modal and show correct labels', testpluralLabels);
it('should open the delete modal with correct entities', testEntities);
it('should only delete images that are valid', testValids);
@ -127,6 +128,18 @@
////////////
function testSingleObject() {
var images = generateImage(1);
service.perform(images[0]);
$scope.$apply();
var labels = deleteModalService.open.calls.argsFor(0)[2].labels;
expect(deleteModalService.open).toHaveBeenCalled();
angular.forEach(labels, function eachLabel(label) {
expect(label.toLowerCase()).toContain('image');
});
}
function testSingleLabels() {
var images = generateImage(1);
service.perform(images);

View File

@ -0,0 +1,55 @@
/**
* (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';
/**
* @ngdoc controller
* @name horizon.app.core.images.DrawerController
* @description
* This is the controller for the images drawer (summary) view.
* Its primary purpose is to provide the metadata definitions to
* the template via the ctrl.metadataDefs member.
*/
angular
.module('horizon.app.core.images')
.controller('horizon.app.core.images.DrawerController', controller);
controller.$inject = [
'horizon.app.core.openstack-service-api.glance',
'horizon.app.core.images.resourceType'
];
var metadataDefs;
function controller(glance, imageResourceType) {
var ctrl = this;
ctrl.metadataDefs = metadataDefs;
if (!ctrl.metadataDefs) {
applyMetadataDefinitions();
}
function applyMetadataDefinitions() {
glance.getNamespaces({resource_type: imageResourceType}, true)
.then(function setMetadefs(data) {
ctrl.metadataDefs = data.data.items;
});
}
}
})();

View File

@ -0,0 +1,49 @@
/**
* (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('drawer controller', function() {
var ctrl;
var glance = {
getNamespaces: angular.noop
};
beforeEach(module('horizon.app.core.images'));
beforeEach(inject(function($controller, $q) {
var deferred = $q.defer();
deferred.resolve({data: {items: []}});
spyOn(glance, 'getNamespaces').and.returnValue(deferred.promise);
ctrl = $controller('horizon.app.core.images.DrawerController',
{
'horizon.app.core.openstack-service-api.glance': glance
}
);
}));
it('checks the metadata for the image type', function() {
expect(glance.getNamespaces).toHaveBeenCalled();
});
it('sets ctrl.metadata', inject(function($timeout) {
$timeout.flush();
expect(ctrl.metadataDefs).toBeDefined();
}));
});
})();

View File

@ -0,0 +1,41 @@
<div ng-controller="horizon.app.core.images.DrawerController as drawerCtrl">
<div class="row">
<dl class="col-md-2">
<dt translate>Is Public</dt>
<dd>{$ item.is_public | yesno $}</dd>
</dl>
<dl class="col-md-2">
<dt translate>Protected</dt>
<dd>{$ item.protected | yesno $}</dd>
</dl>
<dl class="col-md-2">
<dt translate>Format</dt>
<dd>{$ item.disk_format | noValue | uppercase $}</dd>
</dl>
<dl class="col-md-2">
<dt translate>Size</dt>
<dd>{$ item.size | bytes $}</dd>
</dl>
</div>
<div class="row">
<dl class="col-md-2">
<dt translate>Min. Disk</dt>
<dd>{$ item.min_disk | gb | noValue $}</dd>
</dl>
<dl class="col-md-2">
<dt translate>Min. RAM</dt>
<dd>{$ item.min_ram | mb | noValue $}</dd>
</dl>
</div>
<div class="row" ng-if="drawerCtrl.metadataDefs && item.properties">
<div class="col-sm-12">
<metadata-display
available="::drawerCtrl.metadataDefs"
existing="item.properties">
</metadata-display>
</div>
</div>
</div>

View File

@ -0,0 +1,51 @@
/**
* (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('image overview controller', function() {
var ctrl;
var glance = {
getNamespaces: angular.noop
};
beforeEach(module('horizon.app.core.images'));
beforeEach(module('horizon.framework.conf'));
beforeEach(inject(function($controller, $q) {
var deferred = $q.defer();
deferred.resolve({data: {properties: {'a': 'apple'}}});
spyOn(glance, 'getNamespaces').and.returnValue(deferred.promise);
ctrl = $controller('ImageOverviewController',
{
'$scope': {context: {loadPromise: deferred.promise}}
}
);
}));
it('sets ctrl.resourceType', function() {
expect(ctrl.resourceType).toBeDefined();
});
it('sets ctrl.metadata', inject(function($timeout) {
$timeout.flush();
expect(ctrl.image).toBeDefined();
expect(ctrl.image.properties).toBeDefined();
}));
});
})();

View File

@ -36,17 +36,20 @@
.constant('horizon.app.core.images.validationRules', validationRules())
.constant('horizon.app.core.images.imageFormats', imageFormats())
.constant('horizon.app.core.images.resourceType', 'OS::Glance::Image')
.run(registerImageType)
.run(run)
.config(config);
registerImageType.$inject = [
run.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.openstack-service-api.glance',
'horizon.app.core.images.basePath',
'horizon.app.core.images.resourceType'
];
function registerImageType(registry, imageResourceType) {
function run(registry, glance, basePath, imageResourceType) {
registry.getResourceType(imageResourceType)
.setNames(gettext('Image'), gettext('Images'))
.setSummaryTemplateUrl(basePath + 'details/drawer.html')
.setProperty('checksum', {
label: gettext('Checksum')
})
@ -62,6 +65,9 @@
.setProperty('id', {
label: gettext('ID')
})
.setProperty('type', {
label: gettext('Type')
})
.setProperty('members', {
label: gettext('Members')
})
@ -109,7 +115,54 @@
})
.setProperty('ramdisk_id', {
label: gettext('Ramdisk ID')
})
.setListFunction(listFunction)
.tableColumns
.append({
id: 'name',
priority: 1,
sortDefault: true,
template: '<a ng-href="{$ \'project/ngdetails/OS::Glance::Image/\' + item.id $}">' +
'{$ item.name $}</a>'
})
.append({
id: 'type',
priority: 1,
filters: ['imageType']
})
.append({
id: 'status',
priority: 1,
filters: ['imageStatus']
})
.append({
id: 'protected',
priority: 1,
filters: ['yesno']
})
.append({
id: 'disk_format',
priority: 2,
filters: ['noValue', 'uppercase']
})
.append({
id: 'size',
priority: 2,
filters: ['bytes']
});
function listFunction() {
return glance.getImages().then(modifyResponse);
function modifyResponse(response) {
return {data: {items: response.data.items.map(addTrackBy)}};
function addTrackBy(image) {
image.trackBy = image.id + image.updated_at;
return image;
}
}
}
}
/**
@ -176,23 +229,10 @@
function config($provide, $windowProvider, $routeProvider) {
var path = $windowProvider.$get().STATIC_URL + 'app/core/images/';
$provide.constant('horizon.app.core.images.basePath', path);
var tableUrl = path + "table/";
var projectTableRoute = 'project/ngimages/';
var detailsUrl = path + "detail/";
var projectDetailsRoute = 'project/ngimages/details/';
// Share the routes as constants so that views within the images module
// can create links to each other.
$provide.constant('horizon.app.core.images.tableRoute', projectTableRoute);
$provide.constant('horizon.app.core.images.detailsRoute', projectDetailsRoute);
$routeProvider
.when('/' + projectTableRoute, {
templateUrl: tableUrl + 'images-table.html'
})
.when('/' + projectDetailsRoute + ':imageId', {
templateUrl: detailsUrl + 'image-detail.html'
});
$routeProvider.when('/project/ngimages/', {
templateUrl: path + 'panel.html'
});
}
})();

View File

@ -22,73 +22,6 @@
});
});
describe('horizon.app.core.images.tableRoute constant', function () {
var tableRoute;
beforeEach(module('horizon.app.core.images'));
beforeEach(inject(function ($injector) {
tableRoute = $injector.get('horizon.app.core.images.tableRoute');
}));
it('should be defined', function () {
expect(tableRoute).toBeDefined();
});
it('should equal to "project/ngimages/"', function () {
expect(tableRoute).toEqual('project/ngimages/');
});
});
describe('horizon.app.core.images.detailsRoute constant', function () {
var detailsRoute;
beforeEach(module('horizon.app.core'));
beforeEach(module('horizon.app.core.images'));
beforeEach(inject(function ($injector) {
detailsRoute = $injector.get('horizon.app.core.images.detailsRoute');
}));
it('should be defined', function () {
expect(detailsRoute).toBeDefined();
});
it('should equal to "project/ngimages/details/"', function () {
expect(detailsRoute).toEqual('project/ngimages/details/');
});
});
describe('$routeProvider should be configured for images', function() {
var staticUrl, $routeProvider;
beforeEach(function() {
module('ngRoute');
angular.module('routeProviderConfig', [])
.config(function(_$routeProvider_) {
$routeProvider = _$routeProvider_;
spyOn($routeProvider, 'when').and.callThrough();
});
module('routeProviderConfig');
module('horizon.app.core');
inject(function ($injector) {
staticUrl = $injector.get('$window').STATIC_URL;
});
});
it('should set table and detail path', function() {
var imagesRouteCallArgs = $routeProvider.when.calls.argsFor(0);
expect(imagesRouteCallArgs).toEqual([
'/project/ngimages/', {templateUrl: staticUrl + 'app/core/images/table/images-table.html'}
]);
var imagesDetailsCallArgs = $routeProvider.when.calls.argsFor(1);
expect(imagesDetailsCallArgs).toEqual([
'/project/ngimages/details/:imageId',
{ templateUrl: staticUrl + 'app/core/images/detail/image-detail.html'}
]);
});
});
describe('loading the module', function () {
var registry;

View File

@ -0,0 +1,4 @@
<hz-resource-panel resource-type-name="OS::Glance::Image">
<hz-resource-table resource-type-name="OS::Glance::Image"
track-by="trackBy"></hz-resource-table>
</hz-resource-panel>

View File

@ -1,161 +0,0 @@
<div ng-controller="horizon.app.core.images.table.ImagesController as table">
<hz-page-header header="{$ 'Images' | translate $}"></hz-page-header>
<table hz-table ng-cloak
st-table="table.images"
st-safe-src="table.imagesSrc"
class="table table-striped table-rsp table-detail">
<thead>
<tr>
<!--
Table-batch-actions:
This is where batch actions like searching, creating, and deleting.
-->
<th colspan="100" class="search-header">
<hz-search-bar group-classes="input-group" icon-classes="fa-search">
<actions allowed="table.imageResourceType.batchActions" type="batch"
result-handler="table.actionResultHandler"></actions>
</hz-search-bar>
</th>
</tr>
<tr>
<!--
Table-column-headers:
This is where we declaratively define the table column headers.
Include select-col if you want to select all.
Include expander if you want to inline details.
Include action-col if you want to perform actions.
-->
<th class="multi_select_column">
<div class="themable-checkbox">
<input id="images-multi-select" type="checkbox" hz-select-all="table.images">
<label for="images-multi-select"></label>
</div>
</th>
<th class="expander"></th>
<th class="rsp-p1" st-sort="name" st-sort-default="name" translate>Image Name</th>
<th class="rsp-p1" st-sort="type" translate>Type</th>
<th class="rsp-p1" st-sort="status" translate>Status</th>
<th class="rsp-p2" st-sort="filtered_visibility" translate>Visibility</th>
<th class="rsp-p2" st-sort="protected" translate>Protected</th>
<th class="rsp-p2" st-sort="disk_format" translate>Format</th>
<th class="rsp-p2" st-sort="size" translate>Size</th>
<th class="actions_column" translate>Actions</th>
</tr>
</thead>
<tbody>
<!--
Table-rows:
This is where we declaratively define the table columns.
Include select-col if you want to select all.
Include expander if you want to inline details.
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">
<td class="multi_select_column">
<div class="themable-checkbox">
<input type="checkbox"
id="images-{$ image.id $}"
ng-model="tCtrl.selections[image.id].checked"
hz-select="image">
<label for="images-{$ image.id $}"></label>
</div>
</td>
<td class="expander">
<span class="fa fa-chevron-right"
hz-expand-detail
duration="200">
</span>
</td>
<td class="rsp-p1"><a ng-href="{$ 'project/ngdetails/' + table.imageResourceType.type + '/' + table.imageResourceType.pathGenerator(image.id) $}">{$ image.name $}</a></td>
<td class="rsp-p1">{$ image | imageType $}</td>
<td class="rsp-p1">{$ image.status | imageStatus $}</td>
<td class="rsp-p2">{$ image.filtered_visibility $}</td>
<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="actions_column">
<!--
Table-row-action-column:
Actions taken here applies to a single item/row.
-->
<actions allowed="table.imageResourceType.itemActions" type="row" item="image"
result-handler="table.actionResultHandler">
</actions>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<!--
Detail-row:
Contains detailed information on this item.
Can be toggled using the chevron button.
Ensure colspan is greater or equal to number of column-headers.
-->
<td class="detail" colspan="100">
<!--
The responsive columns that disappear typically should reappear here
with the same responsive priority that they disappear.
E.g. table header with rsp-p2 should be here with rsp-alt-p2
-->
<div class="row">
<span class="rsp-alt-p2">
<dl class="col-sm-2">
<dt translate>Visibility</dt>
<dd>{$ image.filtered_visibility $}</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Protected</dt>
<dd>{$ image.protected | yesno $}</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Format</dt>
<dd>{$ image.disk_format | noValue | uppercase $}</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Size</dt>
<dd>{$ image.size | bytes $}</dd>
</dl>
</span>
</div>
<div class="row">
<dl class="col-sm-2">
<dt translate>Min Disk (GB)</dt>
<dd>{$ image.min_disk | noValue $}</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Min RAM (MB)</dt>
<dd>{$ image.min_ram | noValue $}</dd>
</dl>
</div>
<div class="row" ng-if="table.metadataDefs && image.properties">
<div class="col-sm-12">
<metadata-display
available="::table.metadataDefs"
existing="::image.properties">
</metadata-display>
</div>
</div>
</td>
</tr>
<tr hz-no-items items="table.images"></tr>
</tbody>
<!--
Table-footer:
This is where we display number of items and pagination controls.
-->
<tfoot hz-table-footer items="table.images"></tfoot>
</table>
</div>

View File

@ -1,192 +0,0 @@
/**
* (c) Copyright 2015 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.table.ImagesController', ImagesTableController);
ImagesTableController.$inject = [
'$q',
'$scope',
'horizon.framework.widgets.toast.service',
'horizon.app.core.images.detailsRoute',
'horizon.app.core.images.resourceType',
'horizon.app.core.openstack-service-api.glance',
'horizon.app.core.openstack-service-api.policy',
'horizon.app.core.openstack-service-api.userSession',
'horizon.framework.conf.resource-type-registry.service',
'horizon.framework.util.actions.action-result.service',
'imageVisibilityFilter'
];
/**
* @ngdoc controller
* @name horizon.app.core.images.table.ImagesTableController
*
* @param {Object} $q
* @param {Object} $scope
* @param {String} detailsRoute
* @param {Object} imageResourceType
* @param {Object} glance
* @param {Object} userSession
* @param {Object} typeRegistry
* @param {Object} imageVisibilityFilter
* @description
* Controller for the images table.
* Serves as the focal point for table actions.
*
* @returns {undefined} No return value
*/
function ImagesTableController(
$q,
$scope,
toast,
detailsRoute,
imageResourceType,
glance,
policy,
userSession,
typeRegistry,
actionResultService,
imageVisibilityFilter
) {
var ctrl = this;
ctrl.detailsRoute = detailsRoute;
ctrl.checked = {};
ctrl.metadataDefs = null;
ctrl.imageResourceType = typeRegistry.getResourceType(imageResourceType);
ctrl.actionResultHandler = actionResultHandler;
typeRegistry.initActions(imageResourceType, $scope);
init();
////////////////////////////////
function init() {
// if user has permission
// fetch table data and populate it
ctrl.images = [];
ctrl.imagesSrc = [];
var rules = [['image', 'get_images']];
policy.ifAllowed({ rules: rules }).then(loadImages, policyFailed);
}
function loadImages() {
ctrl.images = [];
ctrl.imagesSrc = [];
$q.all(
{
images: glance.getImages(),
session: userSession.get()
}
).then(onInitialized);
}
function onInitialized(d) {
ctrl.imagesSrc.length = 0;
angular.forEach(d.images.data.items, function itemFilter (image) {
//This sets up data expected by the table for display or sorting.
image.filtered_visibility = imageVisibilityFilter(image, d.session.project_id);
ctrl.imagesSrc.push(image);
});
// MetadataDefinitions are only used in expandable rows and are non-critical.
// Defer loading them until critical data is loaded.
applyMetadataDefinitions();
}
function policyFailed() {
var msg = gettext('Insufficient privilege level to get images.');
toast.add('info', msg);
}
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 applyMetadataDefinitions() {
glance.getNamespaces({resource_type: imageResourceType}, true)
.then(function setMetadefs(data) {
ctrl.metadataDefs = data.data.items;
});
}
function actionResultHandler(returnValue) {
return $q.when(returnValue, actionSuccessHandler);
}
function actionSuccessHandler(result) { // eslint-disable-line no-unused-vars
// The action has completed (for whatever "complete" means to that
// action. Notice the view doesn't really need to know the semantics of the
// particular action because the actions return data in a standard form.
// That return includes the id and type of each created, updated, deleted
// and failed item.
//
// This handler is also careful to check the type of each item. This
// is important because actions which create non-images are launched from
// the images page (like create "volume" from image).
var deletedIds, updatedIds, createdIds, failedIds;
if ( result ) {
// Reduce the results to just image ids ignoring other types the action
// may have produced
deletedIds = actionResultService.getIdsOfType(result.deleted, imageResourceType);
updatedIds = actionResultService.getIdsOfType(result.updated, imageResourceType);
createdIds = actionResultService.getIdsOfType(result.created, imageResourceType);
failedIds = actionResultService.getIdsOfType(result.failed, imageResourceType);
// Handle deleted images
if (deletedIds.length) {
ctrl.imagesSrc = difference(ctrl.imagesSrc, deletedIds,'id');
}
// Handle updated and created images
if ( updatedIds.length || createdIds.length ) {
// Ideally, get each created image individually, but
// this is simple and robust for the common use case.
// TODO: If we want more detailed updates, we could do so here.
loadImages();
}
// Handle failed images
if ( failedIds.length ) {
// Do nothing for now
}
} else {
// promise resolved, but no result returned. Because the action didn't
// tell us what happened...reload the displayed images just in case.
loadImages();
}
}
}
})();

View File

@ -1,222 +0,0 @@
/**
* (c) Copyright 2015 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 table controller', function() {
var images = [{id: '1', visibility: 'public', filtered_visibility: 'Public'},
{id: '2', is_public: false, owner: 'not_me',
filtered_visibility: 'Shared with Project'}];
var glanceAPI = {
getImages: function () {
return {
data: {
items: [
{id: '1', visibility: 'public', filtered_visibility: 'Public'},
{id: '2', is_public: false, owner: 'not_me',
filtered_visibility: 'Shared with Project'}
]
},
success: function(callback) {
callback({items : angular.copy(images)});
}
};
},
getNamespaces: function () {
return {
then: function (callback) {
callback({data: {items: []}});
}
};
}
};
var policy = { allowed: true };
function fakePolicy() {
return {
then: function(successFn, errorFn) {
if (policy.allowed) { successFn(); }
else { errorFn(); }
}
};
}
function fakeToast() { return { add: angular.noop }; }
var userSession = {
get: function () {
return {project_id: '123'};
}
};
var mockQ = {
all: function (input) {
return {
then: function (callback) {
callback(input);
}
};
},
when: function (input, callback) {
return callback(input);
}
};
var expectedImages = {
1: {id: '1', visibility: 'public', filtered_visibility: 'Public'},
2: {id: '2', is_public: false, owner: 'not_me', filtered_visibility: 'Shared with Project'}
};
var $scope, controller, toastService, detailsRoute, policyAPI;
beforeEach(module('ui.bootstrap'));
beforeEach(module('horizon.framework'));
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.glance', glanceAPI);
$provide.value('horizon.app.core.openstack-service-api.userSession', userSession);
}));
beforeEach(module('horizon.app.core.images'));
beforeEach(module('horizon.dashboard.project'));
beforeEach(module('horizon.dashboard.project.workflow'));
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
beforeEach(inject(function ($injector, _$rootScope_) {
$scope = _$rootScope_.$new();
toastService = $injector.get('horizon.framework.widgets.toast.service');
policyAPI = $injector.get('horizon.app.core.openstack-service-api.policy');
controller = $injector.get('$controller');
detailsRoute = $injector.get('horizon.app.core.images.detailsRoute');
spyOn(toastService, 'add').and.callFake(fakeToast);
spyOn(policyAPI, 'ifAllowed').and.callFake(fakePolicy);
spyOn(glanceAPI, 'getImages').and.callThrough();
spyOn(glanceAPI, 'getNamespaces').and.callThrough();
spyOn(userSession, 'get').and.callThrough();
spyOn(mockQ, 'all').and.callThrough();
}));
function createController() {
return controller('horizon.app.core.images.table.ImagesController', {
toast: toastService,
policyAPI: policyAPI,
glanceAPI: glanceAPI,
userSession: userSession,
$q: mockQ,
$scope: $scope,
'horizon.app.core.images.row-actions.service': { initScope: angular.noop }
});
}
it('should set details route properly', function() {
expect(createController().detailsRoute).toEqual(detailsRoute);
});
it('should invoke initialization apis', function() {
policy.allowed = true;
var ctrl = createController();
expect(policyAPI.ifAllowed).toHaveBeenCalled();
expect(glanceAPI.getImages).toHaveBeenCalled();
expect(userSession.get).toHaveBeenCalled();
expect(ctrl.imagesSrc).toEqual([
expectedImages['1'],
expectedImages['2']
]);
expect(glanceAPI.getNamespaces).toHaveBeenCalled();
});
it('re-queries if no result', function() {
var ctrl = createController();
glanceAPI.getImages.calls.reset();
ctrl.actionResultHandler();
expect(glanceAPI.getImages).toHaveBeenCalled();
});
it('re-queries if updated', function() {
var ctrl = createController();
glanceAPI.getImages.calls.reset();
ctrl.actionResultHandler({updated: [{type: 'OS::Glance::Image', id: 'b'}]});
expect(glanceAPI.getImages).toHaveBeenCalled();
});
it('re-queries if created', function() {
var ctrl = createController();
glanceAPI.getImages.calls.reset();
ctrl.actionResultHandler({created: [{type: 'OS::Glance::Image', id: 'b'}]});
expect(glanceAPI.getImages).toHaveBeenCalled();
});
it('does not re-query if only failed', function() {
var ctrl = createController();
glanceAPI.getImages.calls.reset();
ctrl.actionResultHandler({failed: [{type: 'OS::Glance::Image', id: 'b'}]});
expect(glanceAPI.getImages).not.toHaveBeenCalled();
});
it('should remove deleted images', function() {
var ctrl = createController();
expect(ctrl.imagesSrc).toEqual([
expectedImages['1'],
expectedImages['2']
]);
var result = {
deleted: [ {type: "OS::Glance::Image", id: '1'} ]
};
ctrl.actionResultHandler(result);
expect(ctrl.imagesSrc).toEqual([
expectedImages['2']
]);
});
it('should not remove deleted volumes', function() {
var ctrl = createController();
expect(ctrl.imagesSrc).toEqual([
expectedImages['1'],
expectedImages['2']
]);
var result = {
deleted: [ {type: "OS::Cinder::Values", id: '1'} ]
};
ctrl.actionResultHandler(result);
expect(ctrl.imagesSrc).toEqual([
expectedImages['1'],
expectedImages['2']
]);
});
it('should not invoke glance apis if policy fails', function() {
policy.allowed = false;
createController();
expect(policyAPI.ifAllowed).toHaveBeenCalled();
expect(toastService.add).toHaveBeenCalled();
expect(glanceAPI.getImages).not.toHaveBeenCalled();
});
});
})();