Cindy Lu, software engineer from IBM, explores the newest standards and features of OpenStack Horizon.

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.

//

beoxjpi
Fig 1: This shows the layers of directives/ dependency chain. Red shows external modules we’ve modified.

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