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 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 aCommandButtonGroup
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 fromTableModel
, 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
Column
s. - A
Component
orComponentSource
used to obtain the rows. - 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 moreMgrColumn
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 Row
s 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 aComponent
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, theMgrColumn
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 rejectedPromise
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. Row
s
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 namedsetShowInActionBar
, 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 particularComponent
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
andkeyName
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 correspondingrestoreStateForKey
function, which again will received a deserialized state
object as the argument when it is called. This too may also optionally return aPromise
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 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 existingComponent
via 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 thefrom
parameter:
MgrTypeInfo.make({ from: [ 'control:BooleanWritable', 'control:NumericWritable' ] })
.then(function (mgrInfos) {
// Do something with the MgrTypeInfos
});
To create an array of MgrTypeInfo
s that represent all the concrete types of a
specified base type, pass an additional boolean concreteTypes
parameter to themake
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 MgrTypeInfo
s 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 MgrTypeInfo
s 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 MgrTypeInfo
s 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 anAction
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 theManager
. - 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 createTreeNode
s 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 aTreeTableModel
. 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 Column
s. 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 ofPropertyColumn
s to read values from Component
s or Struct
s 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 ofMgrTypeInfo
s, 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
andStringPoint
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
andsubscriptionDepth
properties in the constructor, as in the example above.
The concrete point manager should override the makeModel
method to return aPointMgrModel
or subclass. The point manager model has a staticgetDefaultNewTypes
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 themakeCommands()
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 amakeModel
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 |