Merge "Adding generic table extensibility"

This commit is contained in:
Jenkins 2016-06-01 21:11:46 +00:00 committed by Gerrit Code Review
commit 5d618bf490
5 changed files with 308 additions and 98 deletions

View File

@ -64,11 +64,13 @@
var properties = {}; var properties = {};
this.setProperty = setProperty; this.setProperty = setProperty;
this.getName = getName; this.getName = getName;
this.setNames = setNames;
this.label = label; this.label = label;
this.format = format; this.format = format;
this.type = type; this.type = type;
this.setLoadFunction = setLoadFunction; this.setLoadFunction = setLoadFunction;
this.load = load; this.load = load;
this.loadFunction = function def() { return Promise.resolve({data: {}}); };
// These members support the ability of a type to provide a function // These members support the ability of a type to provide a function
// that, given an object in the structure presented by the // that, given an object in the structure presented by the
@ -77,6 +79,31 @@
this.setItemNameFunction = setItemNameFunction; this.setItemNameFunction = setItemNameFunction;
this.itemName = itemName; this.itemName = itemName;
// These members support 'global actions' which are actions associated
// with a resource type, but don't need an existing resource in order
// to be run.
this.globalActions = [];
extensibleService(this.globalActions, this.globalActions);
// These members support summary templates which are views that are meant
// to exist in a confined area, such as a drawer in a table row.
this.summaryTemplateUrl = false;
this.setSummaryTemplateUrl = setSummaryTemplateUrl;
// The list function is meant to provide a standard list of a particular
// type, with the data as a result in a promise. For example, Images code
// would register a list function that returns a promise that will resolve
// to all the Images data in list form.
this.listFunction = angular.noop;
this.setListFunction = setListFunction;
// The table columns are an extensible registration of columns of data
// that could be displayed in a table/grid format. The specification
// for the data elements is defined in the code for hz-dynamic-table.
this.tableColumns = [];
extensibleService(this.tableColumns, this.tableColumns);
this.getTableColumns = getTableColumns;
// The purpose of these members is to allow details to be retrieved // The purpose of these members is to allow details to be retrieved
// automatically from such a path, or similarly to create a path // automatically from such a path, or similarly to create a path
// to such a route from any reference. This establishes a two-way // to such a route from any reference. This establishes a two-way
@ -84,7 +111,7 @@
// The path could be used as part of a details route, for example: // The path could be used as part of a details route, for example:
// //
// An identifier of 'abc-defg' would yield '/abc-defg' which // An identifier of 'abc-defg' would yield '/abc-defg' which
// could be used in a details url, such as: // could be used in a details route, such as:
// '/details/OS::Glance::Image/abc-defg' // '/details/OS::Glance::Image/abc-defg'
this.pathParser = defaultPathParser; this.pathParser = defaultPathParser;
this.setPathParser = setPathParser; this.setPathParser = setPathParser;
@ -168,6 +195,62 @@
return this; return this;
} }
/**
* @ngdoc function
* @name setListFunction
* @description
* Sets a function that returns a promise, that resolves with a list
* of all the items of a given type.
* @example
```
myResourceType.setListFunction(loadData);
function loadData() {
return myResourceTypeApi.get();
}
elsewhere:
myResourceType.list().then(doSomethingWithResult);
```
*/
function setListFunction(func) {
this.listFunction = func;
return this;
}
/**
* @ngdoc function
* @name getTableColumns
* @description
* This is a convenience function that provides the table column
* information back, but places a 'title' member on if not already
* present, using the label provided for the resourceType's member of
* the same name.
* This function is a way of making use of the pre-existing registration
* of a human-readable for the given property, so we don't have to specify
* it again in the column registration.
* @example
```
resourceType.setProperty('owner', {
label: gettext('Owner')
});
resourceType.tableColumns.append({'id': 'owner'}); // no 'title'
var columns = resourceType.getTableColumns();
// columns[0] will contain {'id': 'owner', 'title': 'Owner'}
```
*/
function getTableColumns() {
return this.tableColumns.map(mapTableInfo);
function mapTableInfo(x) {
var tableInfo = x;
tableInfo.title = x.title || label(x.id);
return tableInfo;
}
}
/** /**
* @ngdoc function * @ngdoc function
* @name setPathParser * @name setPathParser
@ -271,6 +354,7 @@
+ '/listener/' + descriptor.id + '/listener/' + descriptor.id
} }
var path = resourceType.detailsPath({id: 12, balancerId: 'abasefasdf');
``` ```
*/ */
function setPathGenerator(func) { function setPathGenerator(func) {
@ -278,6 +362,13 @@
return this; return this;
} }
// The reason for this function as opposed to just setting the value
// is solely to make it easy to chain commands.
function setSummaryTemplateUrl(url) {
this.summaryTemplateUrl = url;
return this;
}
// Functions relating item names, described above. // Functions relating item names, described above.
function defaultItemNameFunction(item) { function defaultItemNameFunction(item) {
return item.name; return item.name;
@ -323,6 +414,24 @@
} }
} }
/**
* @ngdoc function
* @name setNames
* @description
* Takes in the singular/plural names used for display.
* @example
```
var resourceType = getResourceType('thing')
.setNames(gettext('Thing'), gettext('Things'));
});
```
*/
function setNames(singular, plural) {
this.names = [singular, plural];
return this;
}
/** /**
* @ngdoc function * @ngdoc function
* @name label * @name label
@ -397,14 +506,42 @@
} }
var resourceTypes = {}; var resourceTypes = {};
// The slugs are only used to align Django routes with heat
// type names. In a context without Django routing this is
// not needed.
var slugs = {};
var defaultSummaryTemplateUrl = false;
var defaultDetailsTemplateUrl = false; var defaultDetailsTemplateUrl = false;
var registry = { var registry = {
getResourceType: getResourceType,
initActions: initActions,
getGlobalActions: getGlobalActions,
setDefaultSummaryTemplateUrl: setDefaultSummaryTemplateUrl,
getDefaultSummaryTemplateUrl: getDefaultSummaryTemplateUrl,
setDefaultDetailsTemplateUrl: setDefaultDetailsTemplateUrl, setDefaultDetailsTemplateUrl: setDefaultDetailsTemplateUrl,
getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl, getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl,
getResourceType: getResourceType, setSlug: setSlug,
initActions: initActions getTypeNameBySlug: getTypeNameBySlug
}; };
function getTypeNameBySlug(slug) {
return slugs[slug];
}
function setSlug(slug, typeName) {
slugs[slug] = typeName;
return this;
}
function getDefaultSummaryTemplateUrl() {
return defaultSummaryTemplateUrl;
}
function setDefaultSummaryTemplateUrl(url) {
defaultSummaryTemplateUrl = url;
return this;
}
function getDefaultDetailsTemplateUrl() { function getDefaultDetailsTemplateUrl() {
return defaultDetailsTemplateUrl; return defaultDetailsTemplateUrl;
} }
@ -424,22 +561,36 @@
return this; return this;
} }
/*
* @ngdoc function
* @name getGlobalActions
* @description
* This is a convenience function for retrieving all the global actions
* across all the resource types. This is valuable when a page wants to
* display all actions that can be taken without having selected a resource
* type, or otherwise needing to access all global actions.
*/
function getGlobalActions() {
var actions = [];
angular.forEach(resourceTypes, appendActions);
return actions;
function appendActions(type) {
actions = actions.concat(type.globalActions);
}
}
/* /*
* @ngdoc function * @ngdoc function
* @name getResourceType * @name getResourceType
* @description * @description
* Retrieves all information about a resource type. If the resource * Retrieves all information about a resource type. If the resource
* type doesn't exist in the registry, this creates a new entry. * type doesn't exist in the registry, this creates a new entry.
* If a configuration is supplied, the resource type is extended to
* use the configuration's properties.
*/ */
function getResourceType(type, config) { function getResourceType(type) {
if (!resourceTypes.hasOwnProperty(type)) { if (!resourceTypes.hasOwnProperty(type)) {
resourceTypes[type] = new ResourceType(type); resourceTypes[type] = new ResourceType(type);
} }
if (angular.isDefined(config)) {
angular.extend(resourceTypes[type], config);
}
return resourceTypes[type]; return resourceTypes[type];
} }
@ -454,6 +605,7 @@
function initActions(type, scope) { function initActions(type, scope) {
angular.forEach(resourceTypes[type].itemActions, setActionScope); angular.forEach(resourceTypes[type].itemActions, setActionScope);
angular.forEach(resourceTypes[type].batchActions, setActionScope); angular.forEach(resourceTypes[type].batchActions, setActionScope);
angular.forEach(resourceTypes[type].globalActions, setActionScope);
function setActionScope(action) { function setActionScope(action) {
if (action.service.initScope) { if (action.service.initScope) {

View File

@ -61,25 +61,19 @@
}); });
it('returns a new resource type object', function() { it('returns a new resource type object', function() {
var value = service.getResourceType('something', {here: "I am"}); var value = service.getResourceType('something');
expect(value).toBeDefined(); expect(value).toBeDefined();
}); });
it('takes the given properties', function() {
var value = service.getResourceType('something', {here: "I am"});
expect(value.here).toEqual('I am');
});
it('has an setProperty function', function() { it('has an setProperty function', function() {
var value = service.getResourceType('something', {here: "I am"}); var value = service.getResourceType('something');
expect(value.setProperty).toBeDefined(); expect(value.setProperty).toBeDefined();
}); });
it('can be called multiple times, overlaying values', function() { it('can be called multiple times', function() {
var value = service.getResourceType('something', {here: "I am"}); var value = service.getResourceType('something');
service.getResourceType('something', {another: "Thing"}); var value2 = service.getResourceType('something');
expect(value.here).toBe('I am'); expect(value).toBe(value2);
expect(value.another).toBe('Thing');
}); });
}); });
@ -88,6 +82,11 @@
expect(service.getDefaultDetailsTemplateUrl()).toBe('/my/path.html'); expect(service.getDefaultDetailsTemplateUrl()).toBe('/my/path.html');
}); });
it('get/setDefaultSummaryTemplateUrl sets/retrieves a URL', function() {
service.setDefaultSummaryTemplateUrl('/my/path.html');
expect(service.getDefaultSummaryTemplateUrl()).toBe('/my/path.html');
});
describe('label', function() { describe('label', function() {
var label; var label;
beforeEach(function() { beforeEach(function() {
@ -161,35 +160,80 @@
}); });
}); });
it("sets and retrieves slugs", function() {
service.setSlug('image', 'OS::Glance::Image');
expect(service.getTypeNameBySlug('image')).toBe('OS::Glance::Image');
});
describe('getName', function() { describe('getName', function() {
it('returns nothing if names not provided', function() { it('returns nothing if names not provided', function() {
var type = service.getResourceType('something', {}); var type = service.getResourceType('something');
expect(type.getName(2)).toBeUndefined(); expect(type.getName(2)).toBeUndefined();
}); });
it('returns plural if count not provided', function() { it('returns plural if count not provided', function() {
var type = service.getResourceType('something', var type = service.getResourceType('something')
{names: ['Name', 'Names']}); .setNames('Name', 'Names');
expect(type.getName()).toBe('Names'); expect(type.getName()).toBe('Names');
}); });
it('returns singular if given one', function() { it('returns singular if given one', function() {
var type = service.getResourceType('something', {names: ["Image", "Images"]}); var type = service.getResourceType('something')
.setNames("Image", "Images");
expect(type.getName(1)).toBe('Image'); expect(type.getName(1)).toBe('Image');
}); });
it('returns plural if given two', function() { it('returns plural if given two', function() {
var type = service.getResourceType('something', {names: ["Image", "Images"]}); var type = service.getResourceType('something')
.setNames("Image", "Images");
expect(type.getName(2)).toBe('Images'); expect(type.getName(2)).toBe('Images');
}); });
}); });
it('manages the tableColumns', function() {
var type = service.getResourceType('something');
type.tableColumns.push({id: "im-an-id"});
type.tableColumns.push({title: "im-a-title"});
expect(type.getTableColumns()).toEqual([{id: "im-an-id", title: "im-an-id"},
{title: "im-a-title"}]);
});
it('manages the globalActions', function() {
var typeA = service.getResourceType('a');
var typeB = service.getResourceType('b');
typeA.globalActions.push({id: "action-a"});
typeB.globalActions.push({id: "action-b"});
expect(service.getGlobalActions()).toEqual([{id: "action-a"}, {id: "action-b"}]);
});
describe('functions the resourceType object', function() { describe('functions the resourceType object', function() {
var type; var type;
beforeEach(function() { beforeEach(function() {
type = service.getResourceType('something'); type = service.getResourceType('something');
}); });
it("sets a default path generator", function() {
expect(type.pathGenerator('hello')).toBe('hello');
});
it("default load function returns a promise", function() {
expect(type.loadFunction()).toBeDefined();
});
it("allows setting a list function", function() {
function list() {
return 'this would be a promise';
}
type.setListFunction(list);
expect(type.listFunction()).toBe('this would be a promise');
});
it("allows setting of a summary template URL", function() {
type.setSummaryTemplateUrl('/my/path.html');
expect(type.summaryTemplateUrl).toBe('/my/path.html');
expect(type.setSummaryTemplateUrl('/what')).toBe(type);
});
it('itemName defaults to returning the name of an item', function() { it('itemName defaults to returning the name of an item', function() {
var item = {name: 'MegaMan'}; var item = {name: 'MegaMan'};
expect(type.itemName(item)).toBe('MegaMan'); expect(type.itemName(item)).toBe('MegaMan');

View File

@ -37,6 +37,7 @@
*/ */
var horizonBuiltInModules = [ var horizonBuiltInModules = [
'horizon.app.core', 'horizon.app.core',
'horizon.app.resources',
'horizon.app.tech-debt', 'horizon.app.tech-debt',
'horizon.auth', 'horizon.auth',
'horizon.framework' 'horizon.framework'

View File

@ -43,11 +43,7 @@
], config) ], config)
// NOTE: this will move into the correct module as that resource type // NOTE: this will move into the correct module as that resource type
// becomes available. For now there is no volumes module. // becomes available. For now there is no volumes module.
.constant('horizon.app.core.volumes.resourceType', VOLUME_RESOURCE_TYPE) .constant('horizon.app.core.volumes.resourceType', VOLUME_RESOURCE_TYPE);
.run([
'horizon.framework.conf.resource-type-registry.service',
performRegistrations
]);
config.$inject = ['$provide', '$windowProvider', '$routeProvider']; config.$inject = ['$provide', '$windowProvider', '$routeProvider'];
@ -61,71 +57,4 @@
}); });
} }
function performRegistrations(registry) {
// The items in this long list of registrations should ideally placed into
// respective module declarations. However, until they are more fully
// fleshed out there's no reason to pollute the directory/file structure.
// As a model, the Images registration happens in the images module.
registry.getResourceType('OS::Glance::Metadef', {
names: [gettext('Metadata Definition'), gettext('Metadata Definitions')]
});
registry.getResourceType('OS::Nova::Server', {
names: [gettext('Instance'), gettext('Instances')]
});
registry.getResourceType('OS::Nova::Flavor', {
names: [gettext('Flavor'), gettext('Flavors')]
});
registry.getResourceType('OS::Nova::Hypervisor', {
names: [gettext('Hypervisor'), gettext('Hypervisors')]
});
registry.getResourceType('OS::Nova::Keypair', {
names: [gettext('Key Pair'), gettext('Key Pairs')]
});
registry.getResourceType('OS::Designate::Zone', {
names: [gettext('DNS Domain'), gettext('DNS Domains')]
});
registry.getResourceType('OS::Designate::RecordSet', {
names: [gettext('DNS Record'), gettext('DNS Records')]
});
registry.getResourceType('OS::Cinder::Backup', {
names: [gettext('Volume Backup'), gettext('Volume Backups')]
});
registry.getResourceType('OS::Cinder::Snapshot', {
names: [gettext('Volume Snapshot'), gettext('Volume Snapshots')]
});
registry.getResourceType(VOLUME_RESOURCE_TYPE, {
names: [gettext('Volume'), gettext('Volumes')]
});
registry.getResourceType('OS::Swift::Account', {
names: [gettext('Object Account'), gettext('Object Accounts')]
});
registry.getResourceType('OS::Swift::Container', {
names: [gettext('Object Container'), gettext('Object Containers')]
});
registry.getResourceType('OS::Swift::Object', {
names: [gettext('Object'), gettext('Objects')]
});
registry.getResourceType('OS::Neutron::HealthMonitor', {
names: [gettext('Network Health Monitor'), gettext('Network Health Monitors')]
});
registry.getResourceType('OS::Neutron::Net', {
names: [gettext('Network'), gettext('Networks')]
});
registry.getResourceType('OS::Neutron::Pool', {
names: [gettext('Load Balancer Pool'), gettext('Load Balancer Pools')]
});
registry.getResourceType('OS::Neutron::PoolMember', {
names: [gettext('Load Balancer Pool Member'), gettext('Load Balancer Pool Members')]
});
registry.getResourceType('OS::Neutron::Port', {
names: [gettext('Network Port'), gettext('Network Ports')]
});
registry.getResourceType('OS::Neutron::Router', {
names: [gettext('Network Router'), gettext('Network Routers')]
});
registry.getResourceType('OS::Neutron::Subnet', {
names: [gettext('Network Subnet'), gettext('Network Subnets')]
});
}
})(); })();

View File

@ -0,0 +1,84 @@
/*
* (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 overview
* @name horizon.app.resources
* @description
*
* # horizon.app.resources
*
* This module hosts registered resource types. This module file may
* contain individual registrations, or may have sub-modules that
* more fully contain registrations.
*/
angular
.module('horizon.app.resources', [])
.run(performRegistrations);
performRegistrations.$inject = [
'horizon.framework.conf.resource-type-registry.service'
];
function performRegistrations(registry) {
// The items in this long list of registrations should ideally placed into
// respective module declarations. However, until they are more fully
// fleshed out there's no reason to pollute the directory/file structure.
// As a model, the Images registration happens in the images module.
registry.getResourceType('OS::Glance::Metadef')
.setNames(gettext('Metadata Definition'), gettext('Metadata Definitions'));
registry.getResourceType('OS::Nova::Server')
.setNames(gettext('Instance'), gettext('Instances'));
registry.getResourceType('OS::Nova::Flavor')
.setNames(gettext('Flavor'), gettext('Flavors'));
registry.getResourceType('OS::Nova::Hypervisor')
.setNames(gettext('Hypervisor'), gettext('Hypervisors'));
registry.getResourceType('OS::Nova::Keypair')
.setNames(gettext('Key Pair'), gettext('Key Pairs'));
registry.getResourceType('OS::Designate::Zone')
.setNames(gettext('DNS Domain'), gettext('DNS Domains'));
registry.getResourceType('OS::Designate::RecordSet')
.setNames(gettext('DNS Record'), gettext('DNS Records'));
registry.getResourceType('OS::Cinder::Backup')
.setNames(gettext('Volume Backup'), gettext('Volume Backups'));
registry.getResourceType('OS::Cinder::Snapshot')
.setNames(gettext('Volume Snapshot'), gettext('Volume Snapshots'));
registry.getResourceType('OS::Cinder::Volume')
.setNames(gettext('Volume'), gettext('Volumes'));
registry.getResourceType('OS::Swift::Account')
.setNames(gettext('Object Account'), gettext('Object Accounts'));
registry.getResourceType('OS::Swift::Container')
.setNames(gettext('Object Container'), gettext('Object Containers'));
registry.getResourceType('OS::Swift::Object')
.setNames(gettext('Object'), gettext('Objects'));
registry.getResourceType('OS::Neutron::HealthMonitor')
.setNames(gettext('Network Health Monitor'), gettext('Network Health Monitors'));
registry.getResourceType('OS::Neutron::Net')
.setNames(gettext('Network'), gettext('Networks'));
registry.getResourceType('OS::Neutron::Pool')
.setNames(gettext('Load Balancer Pool'), gettext('Load Balancer Pools'));
registry.getResourceType('OS::Neutron::PoolMember')
.setNames(gettext('Load Balancer Pool Member'), gettext('Load Balancer Pool Members'));
registry.getResourceType('OS::Neutron::Port')
.setNames(gettext('Network Port'), gettext('Network Ports'));
registry.getResourceType('OS::Neutron::Router')
.setNames(gettext('Network Router'), gettext('Network Routers'));
registry.getResourceType('OS::Neutron::Subnet')
.setNames(gettext('Network Subnet'), gettext('Network Subnets'));
}
})();