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:
parent
30e37aaf2c
commit
7e6ca1a6cc
@ -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()
|
||||
|
@ -47,7 +47,8 @@
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
resourceTypeName: '@'
|
||||
resourceTypeName: '@',
|
||||
trackBy: '@?'
|
||||
},
|
||||
bindToController: true,
|
||||
templateUrl: basePath + 'table/hz-resource-table.html',
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
@ -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();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -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>
|
@ -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();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -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,22 +229,9 @@
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
4
openstack_dashboard/static/app/core/images/panel.html
Normal file
4
openstack_dashboard/static/app/core/images/panel.html
Normal 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>
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
Loading…
Reference in New Issue
Block a user