Bajaux Manager Framework
Contents
- Introduction
- The Manager Type
- MgrModel
- Component Sources
- MgrColumn
- Rows
- Commands
- Manager State
- MgrTypeInfo
- Discovery
- Subscribers
- Point Manager
- Device Manager
- Glossary
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 change 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.
The Manager type has two methods that can be overridden to handle two table events that are
commonly registered to in the managers. These are:
onMainTableDblClicked()onTableSelectionChanged()
Each of these will be described separately below.
###onMainTableDblClicked()
The onMainTableDblClicked method will be called when a row in the main table in the manager is
double-clicked. It will take as input the double-clicked event and the subjects that were
selected in the table when the double-click event was triggered, and returns a Promise.
This is mainly used to trigger a command on a double-click. For example, some supplied
managers will trigger an EditCommand on the double-click of the selected rows by default.
An example of how this could be overridden is below.
/**
* @param {JQuery.Event} event the double click event
* @param {Array.<*>} subjects the selected subject of the table being clicked
* @return {Promise|*}
*/
MyManager.prototype.onMainTableDblClicked = function (event, subjects) {
//logic to execute on main table double-clicked
const commandGroup = this.getCommandGroup();
const editCommand = commandGroup.find(EditCommand);
return editCommand.invoke();
};
###onTableSelectionChanged()
The onTableSelectionChanged method is called when the selection in the MainTable has changed
and/or in the case where the Learn/Discovery table has been implemented, and its selection has
changed. It takes as input an object that contains two arrays containing the subjects of the
selected rows, and returns a Promise. The first of these arrays is called mainTableSelection,
which contains the subjects of the selected rows in the main table, and the second is
learnTableSelection, which contains the learn table selected row subjects. The
learnTableSelection value will be undefined if the Learn/Discovery table has not been implemented
or the getLearnTable() function returns undefined.
The primary use of this method is to enable/disable commands or other widgets when the rows
selections have changed. For example, enabling the EditCommand when user has selected one or
more rows, or enabling the MatchCommand when the user has selected only one row in each table.
The following is an example of how this can be done.
/**
* @param {Object} selectedSubjects an object that holds the array of the selected subjects from the tables
* supported by the manager view.
* @param {Array.<*>|undefined} selectedSubjects.mainTableSelection the current selected subjects in the main table.
* @param {Array.<*>|undefined} selectedSubjects.learnTableSelection the current selected subjects in the learn
* or discovery table. This will be undefined if the Manager does not support a
learn table or the getLearnTable() function returns undefined.
* @returns {Promise|*}
*/
MyManager.prototype.onTableSelectionChanged = function (selectedSubjects) {
//calling the super procedure
return Manager.prototype.onTableSelectionChanged.call(this, selectedSubjects)
.then(() => {
//logic to execute on table selection changed
const { mainTableSelection, learnTableSelection } = selectedSubjects;
const mainLength = mainTable.length;
const learnLength = learnTable.length;
const commandGroup = this.getCommandGroup();
const matchCmd = commandGroup.find(MatchCommand);
matchCmd.setEnabled(mainLength === 1 && learnLength === 1);
});
};
One thing to make note of, if you are extending a class that already implements the
onTableSelection method, if you do not call that method in the super class you will lose any
functionality that it gives you. Also, if you do call it, it should be called first and then your
logic should be applied, otherwise it could set the enabled state of the commands contrary to
how you want them set.
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:
- One or more
Columns. - A
ComponentorComponentSourceused to obtain the rows. - An array of
MgrTypeInfoinstances 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
Columntype 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 to 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.
To configure exactly where a Manager a command should be made accessible, use
MgrCommand.flags. These flags allow Commands to be placed in the toolbars and
context menus within the Manager.
(All members of Command.flags are also accessible through MgrCommand.flags,
for ease of use.)
See the documentation for Manager#makeCommands for information about the use
of these flags.
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 may 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 receive 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.
As part of the restore process there is an optional function that can be defined,
postRestore. This function can be used to do any post restore processing that might
be necessary. This will receive the full state object as the argument when it is called,
and it may also optionally return a Promise if its process is asynchronous.
/**
* Does any post processing that might be necessary in a restore
*/
MyManager.prototype.postRestore = function (state) {
// Does any post restore processing that might be needed, possibly
// returning a Promise...
};
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.Typeto be used as a base type, which will
return MgrTypeInfo instances for the concrete subclasses of that type. - From a
Componentinstance to be used as a 'prototype' for theMgrTypeInfo.
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
Componentvia itsnewCopy()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
doDiscovermethod invokes anActionon 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
Managerwill then wait for the event to signal that discovery is
complete. - Once the job is complete, the
Managerwill 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 be 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.
**In some circumstances where the discovery item is something that can not be added
an empty array or 'undefined' may be returned. This will cause the Add command to
not include it in the list to be added, and in the case of that being the only item
selected, the Add command will show a dialog to confirm that no selected items could
be added.
/**
* 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.
Note that the second argument to the function will be the corresponding value
in the station database. If using the Add command, this will be a brand-new instance
about to be added to the station. If using the Match command, this will be the existing
instance already in the station.
/**
* Return a proposed name for the new Component, and proposed initial values
* for the 'id', 'enabled' and 'facets' columns.
* @param {baja.Value} discoveredObject
* @param {baja.Component} component
*/
MyManager.prototype.getProposedValuesFromDiscovery = function (discoveredObject, component) {
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();
};
isMatchable()
The isMatchable() method is an optional method that can be implemented. The
purpose of this method is to determine if the subject selected in the Learn table
can be matched to the subject selected in the Main table. The result should either
be a boolean or a Promise that resolves to a boolean. This result is then used
to determine if the MatchCommand should be enabled or disabled.
By default this is done by taking the learn/discovery subject and calling
getTypesForDiscoverySubject() on it and making sure the type of the main subject is
one of the types that is returned from that method. But in some cases that is either not
enough or there are totally different rules that apply to determine if the two subjects
can be matched. In those cases is it necessary to override this method to include
your own code.
To totally override the method you would do something like this.
/**
* The learn subject can match to anything but a folder.
* @param {*} learnSelection the subject selected in the learn table
* @param {baja.Component} mainSelection the subject from the mainSelection that the
* learnSelection subject is being matched to
* @returns {boolean}
*/
MyManager.prototype.isMatchable(learnSelection, mainSelection) {
return !mainSelection.getType().is('baja:Folder');
}
If you need to extend the default logic in some manner, then you can do something along these lines.
/**
* The learn can not be matched if isExisting() returns true, meaning it is already matched.
* @param {*} learnSelection the subject selected in the learn table
* @param {baja.Component} mainSelection the subject from the mainSelection that the
* learnSelection subject is being matched to
* @returns {Promise.<boolean>}
*/
MyManager.prototype.isMatchable(learnSelection, mainSelection) {
return MgrLearn.prototype.isMatchable.apply(this, arguments)
.then((matchable) => {
return matchable && !isExisting(learnSelection);
});
}
Notice that in the first example, since it was not necessary to return a Promise, just a boolean
was returned. But in the second since the default isMatchable returns a Promise that resolves
to a boolean, it was necessary for that method to also return a Promise. Just remember you need to
return either one of those as failing to do so could cause issues with the code not working as it
should.
Additional discovery methods
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.
There is also an optional method that can be implemented if it is needed:
onLearnTableDblClicked()
This method provides a way to perform an action on the double-click of one or more learn/discovery
table rows. Its structure is identical to the the onMainTableDblClicked() above that can
be implemented on the main table. The biggest difference is that the MgrLearn implements this
method and will invoke the AddCommand by default, if it exists. If you override this method, and
still want the AddCommand to be invoked, you must invoke it yourself.
Also, the MgrLearn provides a default onTableSelectionChanged handler method.
This default method will enable the AddCommand when one or more rows are selected in the
Learn/Discoverytable and will enable the MatchCommand if one and only one row is selected in
both tables. If you want this default functionality and have overridden this method in your
manager, make sure you call the super method correctly as mentioned above in the
onTableSelectionChanged() section.
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 |