Managers

Bajaux Manager Framework

Contents

Introduction

For a number of years, Niagara's bajaui manager framework has provided drivers
and other component containers with a common, consistent user interface
framework that allows a user to add, edit and delete components in a station. It
also provides a user with a familiar way to discover new items and add them to
the station using a simple drag and drop process between two tables. The bajaux
manager framework exists to provide a similar, consistent batch editing
framework using HTML5 technologies, allowing the same style of configuration to
be performed by a user in a browser environment, but without the need for a Java
runtime.

The manager framework is based around the concept of a view containing one or
more tables. Niagara drivers are consumers of the manager APIs, with views
provided to allow a user to configure devices and points within a station. As an
example of the manager views in use, a user may use a manager to discover the
points in a remote device via a protocol implemented by the driver. Once
complete, the view's discovery table will display the points found during the
discovery. The user can then pick points to add to the station, with the manager
containing code for creating and configuring the proxy extensions for the
points. The manager can configure the component from the discovered item's
properties, but also provides editing capabilities to allow a user to adjust the
properties, as required.

As with the bajaui version, the bajaux manager framework is implemented around
table widgets and their corresponding models. A bajaux Manager will, at a
minimum, create a model for a main table, providing a number of columns that
describe the values that should be shown and which of those values should be
available for editing. In addition to this basic functionality, the manager may
optionally provide the support for discovery of new items, which will require
the creation of a second model for the discovery table. As with the Java
version, these tables are arranged with the discovery table on top and the main
table at the bottom. A user may choose an item from the upper discovery table,
and then by dragging and dropping onto the lower database table, or by using the
'Add' command, can edit the properties of a newly created Component before it
is added to the station.


Warning: This document describes an API that should currently be considered
experimental (development level stability). The current feature set does
not have complete parity with the Java manager framework and the JavaScript API
may be subject to changes in future releases as more functionality is added. Any
third-party code written against this API may require changes to function
correctly with future releases of Niagara.

The Manager Type

The base of the UX manager framework is a JavaScript type called Manager. This
is a bajaux Widget type that will create a child Table widget for the main
table and load the model into it. In addition to this, it also creates a
CommandButtonGroup widget for the manager's commands, which will be arranged
horizontally along the bottom edge of the view. It also provides some
functionality to save manager state temporarily and restore it again. The
functionality provided by the base Manager type is relatively small; extra
functionality is provided by derived classes and by the use of mixed-in
JavaScript modules.

The Manager type can be accessed by requiring it from the webEditors-ux
module:

define(['nmodule/webEditors/rc/wb/mgr/Manager'], function (Manager) {

The constructor of the Manager type requires some parameters to be passed via
object properties. The required parameters are as follows:

  • moduleName - a string with the name of the module that contains the new
    manager type.
  • keyName - a string to identify the manager, typically the name of the type.

These values should be specified when the constructor of the derived class calls
the super class constructor.

var MyManager = function MyManager (params) {
  Manager.call(this, {
    moduleName: 'myModule',
    keyName: 'MyManager' // Typically the name of the type
  });
};

The key and module names are used for the purposes of deciding a lexicon to load
strings from, when necessary, and are also used to define a key for storing
manager state, thus requiring them to be unique for each concrete manager type.

As with any other bajaux Widget acting as a view on a component, the manager
type must have a corresponding Java type implementing the BIJavaScript
interface (and in a manager's case should implement BIFormFactorMax, too), and
the Java type should be registered as an agent on the relevant component type
via the module-include.xml file.

MgrModel

Each concrete manager type must define a model for its main table. In bajaux,
the MgrModel type provides the base class for main table models. Derived from
TableModel, it adds some extra functionality for creating new component
instances and adding them to the station. The MgrModel type's constructor
requires:

  1. One or more Columns.
  2. A Component or ComponentSource used to obtain the rows.
  3. An array of MgrTypeInfo instances representing the types of any new objects
    that the manager may create.

For any Manager, the makeModel method must be implemented. It should resolve
to an instance of MgrModel, or a subclass of it.

///// MyManager.js:

/**
 * @param {baja.Component} component the component being loaded into the Manager
 * @returns {Promise.<MgrModel>}
 */
MyManager.prototype.makeModel = function (component) {
  return MyMgrModel.make(component);
};
///// MyMgrModel.js:

var TYPES_MY_MANAGER_CAN_CREATE = [
  'control:BooleanWritable', 'control:NumericWritable'
];

//it is permitted, but not required, to subclass MgrModel.
var MyMgrModel = function MyMgrModel () {
  MgrModel.apply(this, arguments);
};
MyMgrModel.prototype = Object.create(MgrModel.prototype);
MyMgrModel.prototype.constructor = MyMgrModel;

/** @returns {Promise.<MgrModel>} */
MyMgrModel.make = function (component) {
  return MgrTypeInfo.make(TYPES_MY_MANAGER_CAN_CREATE)
    .then(function (newTypes) {
      return new MyMgrModel({
          columns: makeColumns(),      // An array of columns for the model
          componentSource: component,  // The component being loaded into the manager or a component source
          newTypes: newTypes           // The types that the manager may create new instances of
      });
    });
};

In the above example, the makeColumns function would instantiate one or more
MgrColumn types and return them in an array.

The MgrModel you create can be accessed as soon as doLoad() is called using
the getModel method. If overriding doLoad(), be sure to call the super
method as Manager#doLoad() provides important functionality.

MyManager.prototype.doLoad = function (component) {
  var model = this.getModel();
  model.getRows().forEach(function (row) { /* ... */ });

  // be sure to call super.
  return Manager.prototype.doLoad.apply(this, arguments);
};

Component Sources

A manager model needs a way to obtain the initial set of Rows it should
contain. In bajaux, when viewing components of a station, this is provided by an
instance of a ComponentSource. The most common type of source used for manager
models will be a ContainerComponentSource, which uses the child property
values of a parent container as the subjects for the model's rows. If a
Component is passed to the model constructor, rather than a ComponentSource,
then a ContainerComponentSource will be created automatically as the default.
In addition to returning the rows for the model, the source also has the
responsibility for adding or removing items from the container.

One important feature of the ContainerComponentSource to note is the filter
functionality. The source's default behavior is to return all visible children
of the parent container (by checking each slot's flags). This may be appropriate
in many cases, but in others it may be necessary to have finer control over
which children are used for the table rows. The ContainerComponentSource
provides for this by taking an optional filter parameter in its constructor.

This filter may take one of two forms: an array of type specs to identify types
that should be allowed for the table's rows, or a predicate function, called for
each Slot on the parent container and receiving the slot as a parameter, which
should return true for the children that should be included in the model.

Taking the example MgrModel defined above, it could be modified to
filter out components via the array method:

var TYPES_MY_MANAGER_SHOULD_DISPLAY = [
  'control:ControlPoint', 'driver:PointFolder'
];

MyMgrModel.make = function (component) {
  return MgrTypeInfo.make(TYPES_MY_MANAGER_CAN_CREATE)
    .then(function (newTypes) {
      return new MyMgrModel({
        columns: makeColumns(),
        componentSource: new ContainerComponentSource({
          container: component,
          filter: TYPES_MY_MANAGER_SHOULD_DISPLAY
        }),
        newTypes: newTypes
      });
    });
};

It could also filter its rows by passing a function as the filter parameter:

function filterComponentsByTypeAndVisibility(prop) {
  var visible = !(prop.getFlags() & baja.Flags.HIDDEN),
      type = prop.getType();

  return visible && (type.is('control:ControlPoint') || type.is('driver:PointFolder'));
}

//...
  componentSource: new ContainerComponentSource({
    container: component,
    filter: filterComponentsByTypeAndVisibility
  })
//...

MgrColumn

A manager's table model must define one or more columns to define exactly what
should be displayed for each row's subject and, if the column supports editing
the value, how a modified value should be saved for a subject. All columns are
derived from a base Column type. This is a generic table column type and is
usable outside of manager views. For manager specific functionality, the
MgrColumn type is used.

The MgrColumn type can be used in one of two ways:

  • As the direct base class for a new type of manager column.
  • As a mixin to augment a more generic Column type with the functionality
    required to be used in a manager model.

To use it as a direct base class, set up the prototype and apply the constructor
in the usual way:

// Create a new manager column, directly inheriting from MgrColumn

var MyMgrColumn = function MyMgrColumn () {
  MgrColumn.apply(this, arguments);
};
MyMgrColumn.prototype = Object.create(MgrColumn.prototype);
MyMgrColumn.prototype.constructor = MyMgrColumn;

Alternatively, to apply it to another generic Column type that may have uses
in other, non-manager tables, use the static mixin function:

// Create a new manager column type, derived from another non-manager column, mixing in MgrColumn

var MyOtherMgrColumn = function MyOtherMgrColumn () {
  FooColumn.apply(this, arguments);
};
MyOtherMgrColumn.prototype = Object.create(FooColumn.prototype);
MyOtherMgrColumn.prototype.constructor = MyOtherMgrColumn;

MgrColumn.mixin(MyOtherMgrColumn);

The Column base class has a name parameter in the constructor. The column
names are used when setting a component's initial values from a discovered item.
This will be described in the discovery section. The constructor
may also be provided with a separate displayName parameter, to provide a
localized user visible name for the column. If this parameter is not specified,
the name will be used as the display name.

When creating a column, a manager may also wish to set the flags via the
constructor. There are three flags that can be specified:

  • Column.flags.EDITABLE - Use this to indicate that the component editor
    should show the value for the column's value.
  • Column.flags.UNSEEN - Use this to indicate that the column should not be
    visible by default. The user can choose to show it if they wish.
  • Column.flags.READONLY - Use this to indicate that the column's value should
    be shown in the component editor, but should be readonly.

These flags can be bitwise-combined as required for the column.

getValueFor is an abstract method on the Column type that should return the
appropriate value for a given row. All new manager columns must implement this
method.

/**
 * Return this column's value for the given row.
 */
MyMgrColumn.prototype.getValueFor = function (row) {
  var componentInRow = row.getSubject();
  return getSomeValueFrom(componentInRow);
};

Another important method on the Column type is buildCell. This is called
when the table is creating its DOM content. The first parameter is the row, the
second is the jQuery object for the <td> element.

/**
 * Build the dom content for the given row.
 */
MyMgrColumn.prototype.buildCell = function (row, dom) {
  var value = this.getValueFor(row),
      text = getDisplayText(value);

  return Promise.resolve(dom.text(text));
};

As well as displaying a value, a new manager column may also want to provide
support for editing a value. There are several steps involved in editing a
column's value: configuring the field editor, validating a user's change and
committing a change back to the row's subject.

The getConfigFor override point allows the column to set a configuration
object for the field editor before it is built. The default implementation will
coalesce multiple rows into a single value to be provided to the editor as the
value. If a manager requires specialized behavior, it may override this method
and provide the required properties that will be passed to the field editor via
the fe.makeFor() method. See the fe documentation for further details of
editor configuration.

Data validation is a task an editable manager column will almost certainly want
to perform. The mgrValidate method can be overridden to have an opportunity to
inspect the proposed changes for the model's rows and possibly reject them. The
validation method will be passed the model and an array of proposed changes for
the column. Each item in the array will either contain the proposed change to
the row at the same index, or null if there is no change for that particular
row. The method should inspect the values in the array and return a rejected
Promise if any values do not pass the validation criteria.

/**
 * Validate the proposed changes to the rows.
 */
MyMgrColumn.prototype.mgrValidate = function (model, data, params) {
  for (var i = 0; i < data.length; i++) {
    if (!isValid(data[i])) {
      return Promise.reject(new Error('invalid value'));
    }
  }
};

After the edits for a column have been validated, they must be committed back to
the source. The commit method should take the given value and write it to the
subject of the Row, returning a Promise that will resolve when the write is
complete. The framework can support the use of batches when rows are being
committed, which will enable several changes to be sent to the station in a
single network call.

/**
 * Commit the changes back to the station.
 */
MyMgrColumn.prototype.commit = function (value, row, params) {
  var comp = row.getSubject(),
      batch = params && params.batch,
      progressCallback = params && params.progressCallback,
      promise = setValueOnComponent(comp, value, batch);

  if (progressCallback) { progressCallback(MgrColumn.COMMIT_READY); }

  return promise;
};

The webEditors module provides several pre-defined columns that may be useful
for managers:

  • NameMgrColumn: Used to display the name of a row's subject component.
  • IconMgrColumn: Used to display the icon of a row's subject component.
  • PathMgrColumn: Used to display the slot path of a row's subject component.
  • PropertyMgrColumn: Used to create a cell's content from a direct property
    of a row's subject component.
  • PropertyPathMgrColumn: Used to create a cell's content from a descendant
    property of a row's subject component, for example a property on a proxy
    extension for a control point subject.

See the API documentation for those types for further details on their
implementation and usage.

Rows

Rows in the database table are represented by a Row type. A Row has a
subject, which can be a JavaScript object of arbitrary type (it will normally be
a reference to the Component represented by the row), an optional icon, and
optional metadata. The Row is passed as a parameter to many of the Column's
methods, such as when building the DOM content for a cell in the table. In such
a case, the column will call the row's getSubject method to access the
component, whereupon it will use the subject's properties to generate the table
cell content.

New instances of a Row are created by the model's makeRow function. Unlike
columns, it will not normally be necessary to subclass the Row type. Rows
allow keyed data to be temporarily stored against an instance. This could allow
a manager to store a value it may want use later against a row, without having
to subclass the Row type or add direct properties to the row object.

/**
 * Create a new row for the model.
 */
MyMgrModel.prototype.makeRow = function (subject) {
  var row = new Row(subject, subject.getNavIcon());
  row.data('my-meta-data', 'foo');  // Set some data to be used later
  return row;
};

Commands

Manager views use the bajaux Command and CommandGroup types to provide the
commands for the buttons at the bottom of the view and on the toolbar. These are
accessible via the command API provided the base Widget type.

On top of base command functionality, the manager framework provides an optional
mixin called MgrCommand, which can be used to extend the base Command type
with extra features. The MgrCommand mixin provides a function named
setShowInActionBar, which can be used to indicate a command should be
available in the toolbar, but not in the action bar at the bottom of the view.
This would normally be used for commands that are not frequently used. The
'discovery mode' command, which is used to toggle the visibility of the
discovery table, is an example of the use of this mixin.

  // Just show the command on the toolbar, not in the action bar at the bottom of the view.
  myCommand.setShowInActionBar(false);

Manager State

The Manager type provides the ability to save state data temporarily, so that
certain aspects of the manager's state can be restored when hyperlinking back to
a previously visited Manager view. The user's web browser will store the state
in session storage, which will preserve the state for the duration of the
session; after a browser or Workbench restart, the state will have been
discarded.

By default a small amount of information is saved by the Manager base class.
The manager will remember which columns are currently shown or hidden, and will
store whether the discovery table is currently visible, if the manager has
discovery support mixed in. The Manager class allows for a couple of override
points that give a derived class the opportunity to save its own custom state,
should it wish to. A typical example might be a driver saving discovery data,
meaning that returning to the manager view for a particular network does not
require the user to perform a re-discovery (which might perhaps be time
consuming, depending on the nature of the system the driver is communicating
with).


The storage provided by the Manager class is intended for simple, transient
state for the user interface. The storage mechanism should not be
considered secure
and must not be used to store sensitive information
such as passwords, private keys or authorization tokens.

The first way a manager can add support for saving data is to add a function
named saveStateForOrd to the Manager's prototype. This is intended to be
used in the situation where the state is only appropriate for a particular
Component instance - the Component's ORD will be keyed against the data.
This might be used in a case where device specific data is to be cached, for
example the discovery data for a device, which has no relevance for other
devices of the same type. This function should return an object with properties
that the manager wants to be stored:

/**
 * Return the state that should be saved, keyed against the current Manager view's ord base.
 */
MyManager.prototype.saveStateForOrd = function () {
  return {
    discoveryConfig: {
      discoverInputs: true,
      discoverOutputs: false
    }
  };
};

Another option is to add a function to the prototype called saveStateForKey.
This allows data to be cached against a particular type of manager view, and can
be restored for any instance of that manager. This uses the moduleName and
keyName parameters passed to the constructor. Again, this should return an
object containing the properties to be stored:

/**
 * Return that state that should be saved for any instances of this Manager type.
 */
MyManager.prototype.saveStateForKey = function () {
  return {
    discoveryTimeout: 10000
  };
}

A Manager that implements either of the above functions will also want to
provide corresponding functions to restore that state when the view is reloaded.

If it provides a saveStateForOrd function, then a Manager should also
provide a restoreStateForOrd function, too. This function's argument will be a
deserialized object containing the state that had previously been saved. The
function may optionally return a Promise if the restoration of the state
requires some asynchronous work to be performed.

/**
 * Restore the Manager's state from the deserialized state object.
 */
MyManager.prototype.restoreStateForOrd = function (state) {
  var that = this;
  return that.doSomethingAsynchronous(state)
    .then(function () {
      that.restoreMyState(state);
    });
};

Likewise, a saveStateForKey function should have a corresponding
restoreStateForKey function, which again will received a deserialized state
object as the argument when it is called. This too may also optionally return a
Promise if the restore is asynchronous.

/**
 * Restore the Manager's state from the deserialized state object.
 */
MyManager.prototype.restoreStateForKey = function (state) {
  // Restore the state, possibly returning a Promise...
};

The manager will call these restore functions during the Widget's load()
process. It will be called at a point after the main table has been loaded with
the model.

MgrTypeInfo

The bajaux manager views make use of a type named MgrTypeInfo for representing
the information about new type instances that can be created by the manager.
This is used to represent types that can be created by the 'New' command and
also types that may be created from a particular discovery item. This is similar
to the Java type of the same name used with bajaui manager views.

The MgrTypeInfo class provides a static make() method that can be used to
create instances in one of several ways:

  • From a type spec string or baja.Type (which can be either a single instance
    or an array)
  • From a type spec string or baja.Type to be used as a base type, which will
    return MgrTypeInfo instances for the concrete subclasses of that type.
  • From a Component instance to be used as a 'prototype' for the MgrTypeInfo.
    Note that this is not a prototype in the JavaScript Object prototype sense,
    but is used as a way to create a new instance by cloning an existing
    Component via its newCopy() method.

The MgrTypeInfo.make() method returns a Promise that will resolve to a
single MgrTypeInfo or array of MgrTypeInfos, depending on the input
parameters. The most basic use is to provide a type or array of types in the
from parameter:

  MgrTypeInfo.make({ from: [ 'control:BooleanWritable', 'control:NumericWritable' ] })
    .then(function (mgrInfos) {
      // Do something with the MgrTypeInfos
    });

To create an array of MgrTypeInfos that represent all the concrete types of a
specified base type, pass an additional boolean concreteTypes parameter to the
make method:

  MgrTypeInfo.make({ from: 'driver:Device', concreteTypes: true })
    .then(function (mgrInfos) {
      // Do something with the MgrTypeInfos
    });

The BajaScript registry can be used in to create an array of MgrTypeInfos for
the agents registered on a particular type. The make() method will accept the
result returned by the registry's getAgents() function.

baja.registry.getAgents("type:myModule:MyType")
  .then(function (agentInfos) {
    return MgrTypeInfo.make({
      from: agentInfos
    });
  })
  .then(function (mgrInfos) {
    // Do something with the MgrTypeInfos 
  });

When providing an array of Types or type specs to the make() function, the
resulting array of MgrTypeInfos will be in the same order as the corresponding
types in the 'from' array. A static helper function is provided that can be used
to sort an array of MgrTypeInfos alphabetically according to their display
names. This function can be passed directly to the sort function of the
JavaScript Array type.

typeInfos.sort(MgrTypeInfo.BY_DISPLAY_NAME);

Discovery

A manager that wishes to support dynamic discovery of items can do so by
requiring the MgrLearn mixin:

define([...
        'nmodule/webEditors/rc/wb/mgr/MgrLearn'], function (
        ...,
        addLearnSupport) {

and can then apply it to the manager instance in its constructor:

addLearnSupport(this);

A typical pattern for a Manager's discovery process will be something like
this:

  • The user clicks the 'Discover' button, which calls the doDiscover() method
    on the manager.
  • The doDiscover method invokes an Action on the station, perhaps first
    displaying a dialog to obtain some configuration parameters, if required. This
    action will start a discovery job and return its ORD.
  • The ORD of the discovery job is then passed to the setJob() method on the
    Manager.
  • The Manager will then wait for the event to signal that discovery is
    complete.
  • Once the job is complete, the Manager will obtain the discovered items
    (typically by reading dynamic slots from the job) and use those to create
    TreeNodes for the discovery table.
  • The discovery table is then loaded with the new tree table nodes.

When applying this mixin, a number of methods are required to be implemented by
the concrete manager. These are:

  • makeLearnModel()
  • doDiscover()
  • getTypesForDiscoverySubject()
  • getProposedValuesFromDiscovery()

Each of these will described separately below.

makeLearnModel()

The makeLearnModel method will be called to create a TreeTableModel for the
discovery table. It should return a Promise that will resolve to a
TreeTableModel. The use of a tree table allows a multilevel hierarchy to be
represented in the discovery table; to show a set of objects at the first level
of the tree, and the properties of those objects (name, value, description, etc)
at the second level, for instance. As with the main table model, this requires
defining a set of Columns. TreeTableModel class defines a static make()
method for creating an instance, which is returned via a Promise:

/**
 * Return a Promise that will resolve to the model for the table.
 */
MyManager.prototype.makeLearnModel = function () {
  return MyLearnModel.make();
};
/**
 * A static factory method for the learn model.
 * @returns {Promise}
 */
MyLearnModel.make = function () {
  return TreeTableModel.make({
    columns: createColumns()  // return an array of Columns
  });
};

The learn model may use whatever columns are appropriate. The use of
PropertyColumns to read values from Components or Structs added to the job
as dynamic slots will likely be common pattern.

doDiscover()

The doDiscover function is called in response to the user clicking the
'Discover' button and its implementation should contain the functionality
required to start an asynchronous discovery via some means. As described
earlier, the most typical pattern will be for the function to invoke an Action
slot on a Component which will submit the appropriate discovery job on the
station side, and then return the job's ORD as the return value of the Action.
This ORD will then be set on the manager via the setJob() method, which will
load the job component into the job bar at the top of the view, thus giving a
progress bar indicator for the discovery, and will cause the manager to
subscribe to the job, in order to be informed of its progress.

/**
 * Invoke an Action on the station that will submit a discovery job, then
 * set the returned ORD on the manager
 */
MyManager.prototype.doDiscover = function () {
  var that = this,
      model = that.value(),
      pointExt = model.getComponentSource().getContainer();

  return that.showDiscoveryConfigurationDialog()
    .then(function (config) {
      // invoke an action that will submit a job and return the ORD
      return pointExt.discoverPoints(config);
    })
    .then(function (ord) {
      ord = baja.Ord.make({
        base: baja.Ord.make('station:'),
        child: ord.relativizeToSession()
      });

      return that.setJob(ord);
    });
};

Once the job has completed, in either success, cancellation or failure, the code
added by the mixin will emit a jobcomplete event, which the concrete manager
can attach a handler function for:

var MyManager = function MyManager (params) {
  var that = this;
  Manager.call(that, { moduleName: 'myModule', keyName: 'MyManager' });

  // Add an event handler for the 'jobcomplete' event to know when discovery has
  // finished.
  that.on('jobcomplete', function (job) {
    that.updateLearnTableModelFromJob(job).catch(baja.error);
  });
};

/**
 * Called asynchronously after the job submitted by doDiscover() has
 * finished. This should get the items found in the discovery and
 * update the TreeNodes in the learn table.
 * @returns {Promise}
 */
MyManager.prototype.updateLearnTableModelFromJob = function (job) {
  var that = this;

  return job.loadSlots()
    .then(function () {
      var discoveries = job.getSlots()
        .is('myModule:MyDiscoveryPoint')
        .toValueArray();
      that.updateLearnTableModel(discoveries);
    });
};

/**
 * Function to update the model for the learn table with the discovered
 * items obtained from the job.
 */
MyManager.prototype.updateLearnTableModel = function (discoveries) {
  var model = this.getLearnModel(),
      root = model.getRootNode();

  // Update the model with the discoveries. Create TreeNodes with a value
  // returning the discovered item.
};

getTypesForDiscoverySubject()

In short: what new things can I create from this discovered object?

getTypesForDiscoverySubject is used when the user is creating a new component
in the station from something that has been discovered and displayed in the
discovery table. The function will take the value of the discovered object that
the user wishes to add, and should return a single MgrTypeInfo or an array of
MgrTypeInfos, if the discovery item may have several possible types in the
station. A typical example of multiple types would be the discovery of control
point items. When a user drags a point with a boolean output value, the manager
might return BooleanWritable, BooleanPoint, StringWritable and
StringPoint as potential types. If returning more than one type, the most
appropriate type should be the first item in the array.

/**
 * Return the type(s) suitable for the given discovery item. Some managers may
 * need to inspect the discovery value to return a suitable type or several
 * types.
 * @param {*} discoveredObject
 * @returns {Promise.<MgrTypeInfo[]>}
 */
MyManager.prototype.getTypesForDiscoverySubject = function (discoveredObject) {
  if (discoveredObject.isBoolean()) {
    return MgrTypeInfo.make({ from: [
     'control:BooleanWritable', 'control:BooleanPoint' ]
    });
  } else {
    // handle other data types....
  }
};

getProposedValuesFromDiscovery()

In short: when adding a new point from a discovered object, how should that
point be initially configured?

getProposedValuesFromDiscovery is used to take values found during the
discovery process (point labels or engineering units, for example) and use them
to set the initial values for a new component. These values will be displayed in
the batch editor dialog, allowing the user to further adjust them before the
component is actually added to the station. The method implementation should
return an object with a string property containing the proposed name (the name
property) and an object property containing any proposed values (the values
property). Each property of the values object should have a name that matches
the name of a column in the main table model and its value should be the
proposed value for that column. It is not necessary to propose a value for every
editable column, as any properties on the created component that do not have
proposals will simply use the default slot value.

/**
 * Return a proposed name for the new Component, and proposed initial values
 * for the 'id', 'enabled' and 'facets' columns.
 * @param {baja.Value} discoveredObject
 */
MyManager.prototype.getProposedValuesFromDiscovery = function (discoveredObject) {
  return {
    name: discoveredObject.getPointLabel(),
    values: {
      id: discoveredObject.getPointId(),
      enabled: true,
      facets: makeProposedFacets(discoveredObject.getEngineeringUnits())
    }
  };
};

isExisting()

In short: have I discovered this thing already?

Implementations of the four methods described above are mandatory for discovery
support. The manager may also optionally provide a method on its prototype
called isExisting() to check whether a given item found during discovery
corresponds to a component already existing within the station. This is used to
adjust the row's icon, to give the user a visual indication that the given item
is already represented in the station's database. When invoked, the first
parameter passed to the function will be the value of a node in the discovery
table, the second parameter will be a component in the station. If the component
corresponds to the discovery item, the function should return true.

/**
 * A discovered object is considered existing if its ID corresponds to the ID
 * of a proxy component already in the station.
 * @param {baja.Value} discoveredObject
 * @param {baja.Component} component
 */
MyManager.prototype.isExisting = function (discoveredObject, component) {
  return discoveredObject.getId() === component.getProxyExt().getPointId();
};

The mixin also adds several methods to the Manager that can be used by a
concrete manager class.

  • setJob()
  • getJob()
  • makeDiscoveryCommands()

The setJob method is used to set a discovery job against the manager. It can
be called with either a job Component or its ORD as the parameter. This will
attach the job to the progress bar and cause the manager to listen for events on
the job. See the doDiscover code above for an example of using this method.

The getJob method will return the job passed to setJob.

The makeDiscoveryCommands method is a helper that will create and return five
new commands in an array. These commands can then be added to the Manager's
command group. The returned array of commands will contain:

  • LearnModeCommand - used to toggle the visibility of the discovery pane.
  • DiscoverCommand - used to start a new discovery.
  • CancelDiscoverCommand - used to cancel a currently running discovery.
  • AddCommand - used to add a new component from a discovered item.
  • MatchCommand - used to update an existing component from a discovered item.

Subscribers

The bajaux module provides a subscriber mixin that can be used in conjunction
with a manager view. It can be required as follows:

define(['bajaux/mixin/subscriberMixIn'], function (subscribable) {

and applied in the constructor:

subscribable(this);

This will subscribe to the view's component at the time it is loaded, and will
unsubscribe at the time the view is destroyed. This mixin uses a regular
BajaScript Subscriber by default. In some cases, subscription to the root
container and one or more levels of components under the root may be necessary.
The webEditors-ux module provides a depth subscriber that can be used for this
purpose. It too needs to be required:

define(['bajaux/mixin/subscriberMixIn',
        'nmodule/webEditors/rc/fe/baja/util/DepthSubscriber'], function (
         subscribable,
         DepthSubscriber) {

Then to use it, create an instance and add it to the manager as a property named
$subscriber, before the subscriber mixin is applied:

this.$subscriber = new DepthSubscriber(2);
subscribable(this);

Point Manager

The driver-ux module part contains a type named PointMgr that can be used as
the base class for point manager views. It also provides a corresponding base
class for a point manager model. The model sets up an appropriate default filter
that will pick out points and point folders for inclusion. Its default
implementation has the ability to create a new ControlPoint type, configured
with a proxy extension type specified in the constructor.

A new point manager can be created by extending the base class:

define(['nmodule/driver/rc/wb/mgr/PointMgr'], function (PointMgr) {

  /**
   * Constructor. This specifies a point folder type and a depth to use for a DepthSubscriber
   */
  var MyPointManager = function MyPointManager () {
    PointMgr.call(this, {
      moduleName: 'myModule',
      keyName: 'MyPointManager',
      folderType: 'myModule:MyPointFolder',
      subscriptionDepth: 3
    });
  };

  MyPointManager.prototype = Object.create(PointMgr.prototype);
  MyPointManager.prototype.constructor = MyPointManager;

For developer convenience, PointMgr provides folder support and a depth
subscriber that can be configured just by passing a the folderType and
subscriptionDepth properties in the constructor, as in the example above.

The concrete point manager should override the makeModel method to return a
PointMgrModel or subclass. The point manager model has a static
getDefaultNewTypes method; this can be called to get an array of MgrTypeInfo
types for the four control point data types (boolean, numeric, enum, string) in
both the 'Point' and 'Writable' versions. This can be used to obtain the new
types for the model.

require([...'nmodule/driver/rc/wb/mgr/PointMgrModel'], function (...PointMgrModel) {
  //...
  MyPointManager.prototype.makeModel = function (component) {
    return PointMgrModel.getDefaultNewTypes()
      .then(function (newTypes) {
        return new PointMgrModel({
          columns: makeColumns(),
          component: component,
          newTypes: newTypes,
          folderType: 'myModule:MyPointFolder',
          proxyExtType: 'myModule:MyProxyExt'
        });
      });
  };
});

As with all manager models, a concrete point manager model must provide the
columns in the call to the base class constructor. The PointMgrModel
constructor takes an additional optional parameter named proxyExtType. If this
parameter is specified, then the default implementation of the newInstance
method will create an instance of that proxy extension type and set it on any
new instances of ControlPoint derived types it creates. If the proxyExtType
parameter is not specified in the constructor (perhaps the driver supports
several proxy extension types), then the concrete model should probably override
the newInstance method, and provide an implementation that will configure the
appropriate proxy extension on the new point component.

Something that a concrete point manager might wish to override is the
makeCommands() method. The default implementation will at minimum return an
array containing the 'New' and 'Edit' commands. If a folder type was provided in
the call to the base class constructor then a 'New Folder' command will be added
too. If the manager supports discovery, then the discovery related commands will
be added ('Add', 'Match', 'Toggle Learn Mode', 'Discover', 'Cancel'). This may
be sufficient for some managers, but others may wish to override the method to
add new commands or remove them.

Device Manager

The driver module also provides a base class for device managers and their
models too. The device manager constructor accepts similar parameters to the
point manager: the module and key strings, the subscription depth for the
subscriber and the optional folder type:

define(['nmodule/driver/rc/wb/mgr/DeviceMgr'], function (DeviceMgr) {

  /**
   * Constructor.
   */
  var MyDeviceManager = function MyDeviceManager () {
    DeviceMgr.call(this, {
      moduleName: 'myModule',
      keyName: 'MyDeviceManager',
      folderType: 'myModule:MyDeviceFolder',
      subscriptionDepth: 1
    });
  };

  MyDeviceManager.prototype = Object.create(DeviceMgr.prototype);
  MyDeviceManager.prototype.constructor = MyDeviceManager;

As with the point manager, the concrete device manager should provide a
makeModel method that returns a subclass of DeviceMgrModel.

/**
 * Return a Promise that will resolve to the device model
 */
MyDeviceManager.prototype.makeModel = function (component) {
  return this.getNewTypes()
    .then(function (newTypes) {
      return new MyDeviceManagerModel({
        component: component,
        newTypes: newTypes
      });
    });
};

Like PointMgr, the base DeviceMgr class provides a makeCommands method
(returning the same default command set as the point manager), which can be
overridden to suit the concrete manager's needs.

Glossary

Term Description
Action Bar The set of Command buttons arranged horizontally along the bottom edge of the Manager
Depth Subscriber A Subscriber type, used to subscribe and component and its descendants down to a certain tree depth
Discovery The act of running an automated process to find potential subjects to be added to the database, for example querying a remote device to find all the data points it contains
Job Bar A widget displayed along the top edge of the manager, used to display the progress of the discovery job, and allow the user to cancel it
Learn Synonymous with 'Discovery'
MgrTypeInfo A class used by manager views to represent a type that the manager is capable of creating