The Horizon framework has been evolving with each release since it began Angular development. Widgets now offer a lot of customization, but it can sometimes be difficult to keep up with the rapid changes, especially given the layers of abstraction. This post aims to cover some of the new standards and how everything fits together. It will use creating a new Angular panel to explain some new concepts.
Note: this may become out-of-date at some point because the framework is in a state of flux. Also, you may still create a panel as before, using the HTML template and defining a controller. This post presents a declarative approach that builds upon the existing Horizon framework.
Prerequisites:
- Know how to create a Django dashboard/ panel
- Know how to create an Angular dashboard/ panel (using HTML template)
A quick recap before we dive into the new material… All Angular content should be placed under a static directory in order to be collected by Horizon automatically (when AUTO_DISCOVER_STATIC_FILES = True in your dashboard’s enabled file enabled/.py). For this tutorial, we create a panel called, Flavors.
As before, you should create these files to generate a blank panel.
openstack_dashboard/dashboards/admin/ngflavors/
├── __init__.py
├── panel.py
├── urls.py
└── views.py //Angular point of entry, angular.html picks up our panel.html content
core.module.js should be modified to add ‘horizon.dashboard.core.flavors’ and we should have an enabled file too.
This skeleton file structure should be placed under openstack_dashboard/static/app/core/flavors/
├── panel.html
├── flavors.module.js
└── flavors.module.spec.js
// panel.html (the starting point)
hzResourcePanel
directive takes in a resource type name and creates the wrapper for the panel, including a header (using hzPageHeader
directive) and content to be transcluded. For OpenStack Dashboard, we use HEAT type names for our resource type name, like “OS::Nova::Flavor”
or “OS::Glance::Image”
to associate with a single service API.
hzResourceTable
directive produces a table and various other components for the specific resource type name. The resource type name is associated to a registry which contains all the relevant data for that type. More of that later. This data is then passed down from hzResourceTable
into the hzDynamicTable
directive. hzDynamicTable
directive then generates all the HTML content for a table using a common template. hzResourceTable
controller adds additional functionality including keeping track of changes/ updates to the registry, magic-search events, and actions, and handling them accordingly.
The registry is the backbone of panel generation. The registry pattern creates a container for objects that can be accessible from anywhere via a key. In our scenario, the application-level resourceTypeRegistry
service collects all the data and details for generating a user interface and provides a single place for you to retrieve this information when you build tables, detail views, forms, modals, etc.
Some of the things you may put in the registry are:
- Actions (e.g. “Create Image”)
- Detail views (when you click on an item for more information)
- Search filter facets
- Url to detail drawer template
- List function to retrieve API service promise
- Property information
- labels for table column headers or form elements
- formatting for property values
Some aspects use Horizon’s extensibility service allowing you to add, remove, replace existing items. These include:
- Item actions
- Batch actions
- Global actions
- Detail views
- Table columns
- Filter facets
This makes it simple to create new table content and customize existing table content to your needs. For example, adding a new item action to an existing Angular table. You would simply add this new configuration to the registry and it would be picked up by the hzResourceTable
directive.
To retrieve data from the registry, you would include the dependency, ‘horizon.framework.conf.resource-type-registry.service
‘ and call ‘registryService.getResourceType()
‘.
For our Flavors table example, we place our registry in flavors.module.js.
(function() {
'use strict';
angular
.module('horizon.app.core.flavors', [
'ngRoute',
'horizon.framework.conf',
'horizon.app.core'
])
.constant('horizon.app.core.flavors.resourceType', 'OS::Nova::Flavor')
.run(run)
.config(config);
run.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.flavors.basePath',
'horizon.app.core.flavors.service',
'horizon.app.core.flavors.resourceType'
];
function run(registry, basePath, flavorsService, flavorResourceType) {
registry.getResourceType(flavorResourceType)
.setNames(gettext('Flavor'), gettext('Flavors'))
.setSummaryTemplateUrl(basePath + 'summary.html')
.setProperty('name', {
label: gettext('Flavor Name')
})
.setProperty('vcpus', {
label: gettext('VCPUs')
})
.setProperty('ram', {
label: gettext('RAM'),
filters: ['mb']
})
.setProperty('disk', {
label: gettext('Root Disk'),
filters: ['gb']
})
.setProperty('OS-FLV-EXT-DATA:ephemeral', {
label: gettext('Ephemeral Disk'),
filters: ['gb']
})
.setProperty('swap', {
label: gettext('Swap Disk'),
filters: ['gb']
})
.setProperty('rxtx_factor', {
label: gettext('RX/TX Factor')
})
.setProperty('id', {
label: gettext('ID')
})
.setProperty('os-flavor-access:is_public', {
label: gettext('Public'),
filters: ['yesno']
})
.setProperty('metadata', {
label: gettext('Metadata')
})
.setListFunction(flavorsService.getFlavorsPromise)
.tableColumns
.append({
id: 'name',
priority: 1
})
.append({
id: 'vcpus',
priority: 2
})
.append({
id: 'ram',
priority: 1,
sortDefault: true
})
.append({
id: 'disk',
priority: 2
})
.append({
id: 'id',
priority: 1
})
.append({
id: 'os-flavor-access:is_public',
priority: 2
});
}
config.$inject = [
'$provide',
'$windowProvider',
'$routeProvider'
];
/**
* @name config
* @param {Object} $provide
* @param {Object} $windowProvider
* @param {Object} $routeProvider
* @description Routes used by this module.
* @returns {undefined} Returns nothing
*/
function config($provide, $windowProvider, $routeProvider) {
var path = $windowProvider.$get().STATIC_URL + 'app/core/flavors/';
$provide.constant('horizon.app.core.flavors.basePath', path);
$routeProvider.when('/admin/flavors/', {
templateUrl: path + 'panel.html'
});
}
})();
Then you can access the registry like:
var resourceType = registryService.getResourceType('OS::Glance::Image');
resourceType.getTableColumns();
More details on resource-type-registry service.
//
hzDynamicTable
directive is built off the AngularJS Smart-Table module and requires config
and items
. The config
object contains an array of objects that describes each column. The items
object contains the data to want to show (probably from an API call). You may also pass in batch actions, item actions, filter facets into the directive for more complex use case.
If you take a look at hz-dynamic-table.html, it is essentially what you would write to generate an Angular table prior to Newton release. We packaged that up so that you don’t need to rewrite the same boilerplate code for a new table. Instead, pass in a registry and ‘Voila!’ a table is created!
hzDynamicTable
uses other Horizon framework components:
- hz-magic-search-context
- st-table
- hz-table
- actions
- hz-select-all
- hz-select
- hz-magic-search-bar
- hz-cell (uses hz-field)
- hz-no-items
- hz-detail-row
- hz-expand-detail
- hz-table-footer
More details on hz-dynamic-table.
For each row, you can also have an expandable detail drawer defined by summaryTemplateUrl()
in registry and passed into hzDynamicTable’s config as detailsTemplateUrl.
Inside the detail_drawer.html, you may use hzResourcePropertyList
directive.
This displays set of properties registered to a particular resource type grouped by label/values. It is built off a 12-column system. In the example above, it will generate 4 columns.
Name VCPUs Root Disk RX/TX Factor
m1.tiny 1 1GB 1
ID RAM Ephemeral Disk
123 512MB 0GB
Swap Disk
0GB
The directive also uses the permissions
service to check whether or not to show the column. It will check that certain policies pass, certain settings are enabled, or certain services are enabled. In addition, you can set a generic allowed function on the column config.
//
A summary of the topics covered in this post include:
- Horizon framework directives and how they work together
- Resource type registry service
- Extensibility service
Also, please check out Images Panel for the most up-to-date reference.
Til next time,
Cindy
This post first appeared on the IBM OpenTech blog. Superuser is always interested in community content, email: [email protected].
Cover Photo // CC BY NC
- Exploring the Open Infrastructure Blueprint: Huawei Dual Engine - September 25, 2024
- Open Infrastructure Blueprint: Atmosphere Deep Dive - September 18, 2024
- Datacomm’s Success Story: Launching A New Data Center Seamlessly With FishOS - September 12, 2024