Adding Resource Type registration

This patch adds to the registry service to allow for resource types
to have their properties registered with configuration information.

It provides various convenience methods for outputting the property
labels and values, all determined through the registry, and provides
for singular and plural names to be defined.

This patch also registers many of the OpenStack resource types so
they may be used by implementations.  Ideally those are broken into
component modules, but until those are written they can reside here.

Change-Id: I71bebe253452944ff518f85b8840cfe921602544
Partially-Implements: blueprint angularize-images-table
This commit is contained in:
Matt Borland 2016-02-17 07:59:11 -07:00
parent 058bd9fbf2
commit 8f878166f3
10 changed files with 497 additions and 122 deletions

View File

@ -1,5 +1,5 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
* (c) Copyright 2015 Hewlett Packard Enterprise Development Company LP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -30,7 +30,13 @@
* @name horizon.framework.conf.resource-type-registry.service
* @description
* This service provides a registry which allows for registration of
* configurations for resource types. These configurations include
* configurations for resource types. The purpose of these registrations
* is to make it easy for modules to register a variety of common features
* that are used both in display and behavior related to resource types.
* Ideally the primary members of a resource type are decided on by
* the community; however it is possible using a configuration to add
* all kinds of members to the resource type.
* Common elements in resource type configurations include things like
* batch and item actions, which are associated with the resource type
* via a key. The key follows the format: OS::Glance::Image.
* The functions that are exposed both assist with registration and also
@ -50,65 +56,201 @@
*/
function registryService(extensibleService) {
/*
* @ngdoc function
* @name getMember
* @description
* Given a resource type name and the member type (e.g. 'itemActions')
* This returns the extensible container for those inputs. If either
* requested item hasn't been initialized yet, they are created.
*/
function getMember(type, member) {
if (!resourceTypes.hasOwnProperty(type)) {
resourceTypes[type] = {};
}
if (!resourceTypes[type].hasOwnProperty(member)) {
resourceTypes[type][member] = [];
extensibleService(resourceTypes[type][member],
resourceTypes[type][member]);
function ResourceType() {
// 'properties' contains information about properties associated with
// this resource type. The expectation is that the key is the 'code'
// name of the property and the value conforms to the standard
// described in the setProperty() function below.
var properties = {};
this.setProperty = setProperty;
this.getName = getName;
this.label = label;
this.format = format;
// itemActions is a list of actions that can be executed upon a single
// item. The list is made extensible so it can be added to independently.
this.itemActions = [];
extensibleService(this.itemActions, this.itemActions);
// batchActions is a list of actions that can be executed upon multiple
// items. The list is made extensible so it can be added to independently.
this.batchActions = [];
extensibleService(this.batchActions, this.batchActions);
/**
* @ngdoc function
* @name setProperty
* @description
* Adds a property to the resource type object. Replaces any existing
* definition. These calls can be chained to other ResourceType
* functions. Specific information about the logic and evaluation of
* the property attributes is more fully described in the
* format() function.
* @example
```
resourceType.setProperty("kernel_id", {
label: gettext("Kernel ID") // just provides the label.
// values will be shown directly
})
.setproperty("disk_size", {
label: gettext("disk size"),
value_function: function(size) { // uses function to
return interpolate("%s GiB", size); // display values.
}
})
.setproperty("disk_size", {
label: gettext("disk size"),
value_function: [
function(size) { // uses multiple
return input.replace(/-/, ' '); // functions in sequence
}, // to display values.
function(input) {
return input.toUpperCase();
}
]
})
.setProperty("status", {
label: gettext("Status"),
value_mapping: { // uses mapping to
ready: gettext("Ready"), // look up values
waiting: gettext("Waiting")
},
default_value: gettext("Unknown") // uses default if no match
})
.setProperty("state", {
label: gettext("State"),
value_mapping: { // uses mapping to
initial: gettext("Initial"), // look up values
running: gettext("Running")
},
default_function: function(input) { // uses function if no match
return input.toUpperCase();
}
})
)
```
*/
function setProperty(name, prop) {
properties[name] = prop;
return this;
}
return resourceTypes[type][member];
}
/**
* @ngdoc function
* @name getName
* @description
* Given a count, returns the appropriate name (e.g. singular or plural)
* @example
```
var resourceType = getResourceType('thing', {
names: [gettext('Thing'), gettext('Things')]
});
/*
* @ngdoc function
* @name getMemberFunction
* @description
* Returns a function that returns the requested items. This is only
* present as a utility function for the benefit of the action directive.
*/
function getMemberFunction(type, member) {
return function() { return resourceTypes[type][member]; };
var singleName = resourceType.getName(1); // returns singular
```
*/
function getName(count) {
if (this.names) {
return ngettext.apply(null, this.names.concat([count]));
}
}
/**
* @ngdoc function
* @name label
* @description
* Returns a human-appropriate label for the given name.
* @example
```
var name = resourceType.propLabel('disk_format'); // Yields 'Disk Format'
```
*/
function label(name) {
var prop = properties[name];
if (angular.isDefined(prop) && angular.isDefined(prop.label)) {
return prop.label;
}
return name;
}
/**
* @ngdoc function
* @name format
* @description
* Returns a well-formatted value given a property name and an
* associated value.
* The formatting is determined by evaluating various options on the
* property.
*
* 'value_function' provides a single or a list of functions that will
* evaluate the source value as input and return output; or in the case
* of multiple functions, will chain the output of one to the input of
* the next.
*
* 'value_mapping' provides a hash where, if a matching key is found,
* the value is returned. If no matching key is found, then if
* 'value_mapping_default_function' is present, the value is passed
* to the function and the result is returned. Finally, if there was
* no matching key and no default function, 'value_mapping_default_value'
* provides a string to be returned.
*
* If these options are not present, the original value is returned.
* value.
* @example
```
var value = resourceType.format('disk_size', 12); // Yields '12 GiB'
```
*/
function format(name, value) {
var prop = properties[name];
if (angular.isUndefined(prop)) {
// no property definition; return the original value.
return value;
} else if (prop.value_function) {
if (angular.isArray(prop.value_function)) {
return prop.value_function.reduce(function execEach(prev, func) {
return func(prev);
}, value);
} else {
return prop.value_function(value);
}
} else if (prop.value_mapping) {
if (angular.isDefined(prop.value_mapping[value])) {
return prop.value_mapping[value];
} else if (angular.isDefined(prop.value_mapping_default_function)) {
return prop.value_mapping_default_function(value);
} else if (angular.isDefined(prop.value_mapping_default_value)) {
return prop.value_mapping_default_value;
}
}
// defaults to simply returning the original value.
return value;
}
}
var resourceTypes = {};
var registry = {
getItemActions: getItemActions,
getItemActionsFunction: getItemActionsFunction,
initActions: initActions,
getBatchActions: getBatchActions,
getBatchActionsFunction: getBatchActionsFunction
getResourceType: getResourceType,
initActions: initActions
};
/*
* @ngdoc function
* @name getItemActions
* @name getResourceType
* @description
* Retrieves the type's item actions.
* Retrieves all information about a resource type. If the resource
* 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 getItemActions(type) {
return getMember(type, 'itemActions');
}
/*
* @ngdoc function
* @name getItemActionsFunction
* @description
* Retrieves a function returning the type's item actions.
*/
function getItemActionsFunction(type) {
return getMemberFunction(type, 'itemActions');
function getResourceType(type, config) {
if (!resourceTypes.hasOwnProperty(type)) {
resourceTypes[type] = new ResourceType();
}
if (angular.isDefined(config)) {
angular.extend(resourceTypes[type], config);
}
return resourceTypes[type];
}
/*
@ -130,26 +272,6 @@
}
}
/*
* @ngdoc function
* @name getBatchActions
* @description
* Retrieves the type's batch actions.
*/
function getBatchActions(type) {
return getMember(type, 'batchActions');
}
/*
* @ngdoc function
* @name getBatchActionsFunction
* @description
* Retrieves a function returning the type's batch actions.
*/
function getBatchActionsFunction(type) {
return getMemberFunction(type, 'batchActions');
}
return registry;
}

View File

@ -34,51 +34,147 @@
expect(service).toBeDefined();
});
describe('getItemActions', function() {
it('adds a member when called and no member present', function() {
expect(service.getItemActions('newthing')).toBeDefined();
});
it('sets itemAction when getItemAction is called', function() {
service.getItemActions('newthing').push(1);
service.getItemActions('newthing').push(2);
expect(service.getItemActions('newthing')).toEqual([1, 2]);
});
});
describe('getBatchActions', function() {
it('adds a member when getBatchAction is called an member not present', function() {
expect(service.getBatchActions('newthing')).toBeDefined();
});
it('sets batchAction when addBatchAction is called', function() {
service.getBatchActions('newthing').push(1);
service.getBatchActions('newthing').push(2);
expect(service.getBatchActions('newthing')).toEqual([1, 2]);
});
});
it('returns a function that returns item actions', function() {
service.getItemActions('newthing').push(1);
expect(service.getItemActionsFunction('newthing')()).toEqual([1]);
});
it('returns a function that returns batch actions', function() {
service.getBatchActions('newthing').push(1);
expect(service.getBatchActionsFunction('newthing')()).toEqual([1]);
});
it('init calls initScope on item and batch actions', function() {
var action = { service: { initScope: angular.noop } };
spyOn(action.service, 'initScope');
service.getBatchActions('newthing').push(action);
service.getResourceType('newthing').batchActions.push(action);
service.initActions('newthing', { '$new': function() { return 4; }} );
expect(action.service.initScope).toHaveBeenCalledWith(4);
});
it('init ignores initScope when not present', function() {
var action = { service: { } };
service.getResourceType('newthing').batchActions.push(action);
var returned = service.initActions('newthing', {} );
// but we got here
expect(returned).toBeUndefined();
});
describe('getResourceType', function() {
it('returns a new resource type object even without a config', function() {
var value = service.getResourceType('something');
expect(value).toBeDefined();
});
it('returns a new resource type object', function() {
var value = service.getResourceType('something', {here: "I am"});
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() {
var value = service.getResourceType('something', {here: "I am"});
expect(value.setProperty).toBeDefined();
});
it('can be called multiple times, overlaying values', function() {
var value = service.getResourceType('something', {here: "I am"});
service.getResourceType('something', {another: "Thing"});
expect(value.here).toBe('I am');
expect(value.another).toBe('Thing');
});
});
describe('label', function() {
var label;
beforeEach(function() {
var value = service.getResourceType('something', {})
.setProperty('example', {label: gettext("Example")})
.setProperty('bad_example', {});
label = value.label;
});
it('returns the property name if there is no such property', function() {
expect(label('not_exist')).toBe('not_exist');
});
it('returns the property name if there is no such property label', function() {
expect(label('bad_example')).toBe('bad_example');
});
it('returns the nice label if there is one', function() {
expect(label('example')).toBe('Example');
});
});
describe('format', function() {
var format;
beforeEach(function() {
var value = service.getResourceType('something', {})
.setProperty('mapping', {value_mapping: {'a': 'apple', 'j': 'jacks'}})
.setProperty('func', {value_function: function(x) { return x.replace('a', 'y'); }})
.setProperty('default-func', {value_mapping: {a: 'apple'},
value_mapping_default_function: function(x) { return x.replace('i', 'a'); }})
.setProperty('multi-func', {value_function: [
function(x) { return x.replace('a', 'y'); },
function(x) { return x.replace('y', 'x'); }
]})
.setProperty('default', {value_mapping: {},
value_mapping_default_value: 'Fell Thru'})
.setProperty('bad_example', {});
format = value.format;
});
it('returns the value if there is no such property', function() {
expect(format('not_exist', 'apple')).toBe('apple');
});
it('returns the value if there is no mapping, function, or default', function() {
expect(format('bad_example', 'apple')).toBe('apple');
});
it('returns the mapped value if there is one', function() {
expect(format('mapping', 'a')).toBe('apple');
});
it('returns the function return value if there is a value', function() {
expect(format('func', 'apple')).toBe('ypple');
});
it('returns the multiple function return value if there is an array', function() {
expect(format('multi-func', 'apple')).toBe('xpple');
});
it('returns the default mapping value if there is no mapping or function', function() {
expect(format('default', 'apple')).toBe('Fell Thru');
});
it('returns the original value if there is no matching mapping & no default', function() {
expect(format('mapping', 'what')).toBe('what');
});
it('returns the value_mapping_default_function result when no matching mapping', function() {
expect(format('default-func', 'missing')).toBe('massing');
});
});
describe('getName', function() {
it('returns nothing if names not provided', function() {
var type = service.getResourceType('something', {});
expect(type.getName(2)).toBeUndefined();
});
it('returns plural if count not provided', function() {
var type = service.getResourceType('something',
{names: ['Name', 'Names']});
expect(type.getName()).toBe('Names');
});
it('returns singular if given one', function() {
var type = service.getResourceType('something', {names: ["Image", "Images"]});
expect(type.getName(1)).toBe('Image');
});
it('returns plural if given two', function() {
var type = service.getResourceType('something', {names: ["Image", "Images"]});
expect(type.getName(2)).toBe('Images');
});
});
});
})();

View File

@ -223,7 +223,13 @@
function link(scope, element, attrs, actionsController) {
var listType = attrs.type;
var item = attrs.item;
var allowedActions = $parse(attrs.allowed)(scope)();
var allowedActions;
var actionsParam = $parse(attrs.allowed)(scope);
if (angular.isFunction(actionsParam)) {
allowedActions = actionsParam();
} else {
allowedActions = actionsParam;
}
var service = actionsService({
scope: scope,

View File

@ -37,7 +37,11 @@
'horizon.framework.util',
'horizon.framework.widgets',
'horizon.dashboard.project.workflow'
], config);
], config)
.run([
'horizon.framework.conf.resource-type-registry.service',
performRegistrations
]);
config.$inject = ['$provide', '$windowProvider'];
@ -46,4 +50,71 @@
$provide.constant('horizon.app.core.basePath', path);
}
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('Server'), gettext('Servers')]
});
registry.getResourceType('OS::Nova::Flavor', {
names: [gettext('Flavor'), gettext('Flavors')]
});
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('OS::Cinder::Volume', {
names: [gettext('Volume'), gettext('Volumes')]
});
registry.getResourceType('OS::Nova::Flavor', {
names: [gettext('Flavor'), gettext('Flavors')]
});
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

@ -42,30 +42,31 @@
deleteImageService,
launchInstanceService,
updateMetadataService,
imageResourceType)
imageResourceTypeCode)
{
registry.getItemActions(imageResourceType)
var imageResourceType = registry.getResourceType(imageResourceTypeCode);
imageResourceType.itemActions
.append({
id: 'launchInstanceService',
service: launchInstanceService,
template: {
text: gettext('Launch')
}
}, 500)
})
.append({
id: 'createVolumeAction',
service: createVolumeService,
template: {
text: gettext('Create Volume')
}
}, 200)
})
.append({
id: 'updateMetadataService',
service: updateMetadataService,
template: {
text: gettext('Update Metadata')
}
}, 100)
})
.append({
id: 'deleteImageAction',
service: deleteImageService,
@ -73,9 +74,9 @@
text: gettext('Delete Image'),
type: 'delete'
}
}, -100);
});
registry.getBatchActions(imageResourceType)
imageResourceType.batchActions
.append({
id: 'batchDeleteImageAction',
service: deleteImageService,
@ -83,7 +84,7 @@
type: 'delete-selected',
text: gettext('Delete Images')
}
}, 100);
});
}
})();

View File

@ -21,15 +21,19 @@
ImageDetailController.$inject = [
'horizon.app.core.images.tableRoute',
'horizon.app.core.images.resourceType',
'horizon.app.core.openstack-service-api.glance',
'horizon.app.core.openstack-service-api.keystone',
'horizon.framework.conf.resource-type-registry.service',
'$routeParams'
];
function ImageDetailController(
tableRoute,
imageResourceTypeCode,
glanceAPI,
keystoneAPI,
registry,
$routeParams)
{
var ctrl = this;
@ -38,6 +42,7 @@
ctrl.project = {};
ctrl.hasCustomProperties = false;
ctrl.tableRoute = tableRoute;
ctrl.resourceType = registry.getResourceType(imageResourceTypeCode);
var imageId = $routeParams.imageId;

View File

@ -71,8 +71,8 @@
<hr>
<dl class="dl-horizontal">
<div ng-repeat="prop in ctrl.image.properties">
<dt data-toggle="tooltip" title="{$ prop.name $}">{$ prop.name $}</dt>
<dd>{$ prop.value $}</dd>
<dt data-toggle="tooltip" title="{$ prop.name $}">{$ ctrl.resourceType.label(prop.name) $}</dt>
<dd>{$ ctrl.resourceType.format(prop.name, prop.value) $}</dd>
</div>
</dl>
</div>

View File

@ -30,8 +30,83 @@
.constant('horizon.app.core.images.events', events())
.constant('horizon.app.core.images.non_bootable_image_types', ['aki', 'ari'])
.constant('horizon.app.core.images.resourceType', 'OS::Glance::Image')
.run(registerImageType)
.config(config);
registerImageType.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.images.resourceType'
];
function registerImageType(registry, imageResourceType) {
registry.getResourceType(imageResourceType, {
names: [gettext('Image'), gettext('Images')]
})
.setProperty('checksum', {
label: gettext('Checksum')
})
.setProperty('container_format', {
label: gettext('Container Format')
})
.setProperty('created_at', {
label: gettext('Created At')
})
.setProperty('disk_format', {
label: gettext('Disk Format')
})
.setProperty('id', {
label: gettext('ID')
})
.setProperty('members', {
label: gettext('Members')
})
.setProperty('min_disk', {
label: gettext('Min. Disk')
})
.setProperty('min_ram', {
label: gettext('Min. RAM')
})
.setProperty('name', {
label: gettext('Name')
})
.setProperty('owner', {
label: gettext('Owner')
})
.setProperty('protected', {
label: gettext('Protected')
})
.setProperty('size', {
label: gettext('Size')
})
.setProperty('status', {
label: gettext('Status')
})
.setProperty('tags', {
label: gettext('Tags')
})
.setProperty('updated_at', {
label: gettext('Updated At')
})
.setProperty('virtual_size', {
label: gettext('Virtual Size')
})
.setProperty('visibility', {
label: gettext('Visibility')
})
.setProperty('description', {
label: gettext('Description')
})
.setProperty('architecture', {
label: gettext('Architecture')
})
.setProperty('kernel_id', {
label: gettext('Kernel ID')
})
.setProperty('ramdisk_id', {
label: gettext('Ramdisk ID')
});
}
/**
* @ngdoc value
* @name horizon.app.core.images.events

View File

@ -13,7 +13,7 @@
-->
<th colspan="100" class="search-header">
<hz-search-bar group-classes="input-group" icon-classes="fa-search">
<actions allowed="table.getBatchActions" type="batch"></actions>
<actions allowed="table.imageResourceType.batchActions" type="batch"></actions>
</hz-search-bar>
</th>
</tr>
@ -78,7 +78,7 @@
Table-row-action-column:
Actions taken here applies to a single item/row.
-->
<actions allowed="table.getItemActions" type="row" item="image">
<actions allowed="table.imageResourceType.itemActions" type="row" item="image">
</actions>
</td>
</tr>

View File

@ -62,8 +62,7 @@
ctrl.imagesSrc = [];
ctrl.metadataDefs = null;
ctrl.getItemActions = typeRegistry.getItemActionsFunction(imageResourceType);
ctrl.getBatchActions = typeRegistry.getBatchActionsFunction(imageResourceType);
ctrl.imageResourceType = typeRegistry.getResourceType(imageResourceType);
var deleteWatcher = $scope.$on(events.DELETE_SUCCESS, onDeleteSuccess);