Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

This tutorial will step you through the process of creating an application that does just that - injects a – injects a tabular view into the ONOS GUI.

Application Set Up

Setting up the application is exactly the same as for the Custom View tutorial, with one minor difference: the choice of archetype in step (2) should be uitab instead of ui:

Warning

Currently, the uitab archetype has not been implemented, so the following command will not work until this has been fixed.

 

A fictitious company, "Meowster, Inc." is used throughout the examples. This tutorial builds on top of the Custom View tutorial, although you could choose to only have a table view in your app, if you wished.

Let's get started...

Adding a Tabular View to our App

First of all, let's go back to the top level directory for our sample application:

Code Block
$ cd ~/meow/sample

 

Let's add the table view template files by overlaying the uitab archetype:(2) Overlay the UI additional components

Code Block
$ onos-create-app uitab org.meowster.app.sample meowster-app

Building and Installing the App

sample

When asked for the version, accept the suggested default: 1.0-SNAPSHOT, and press enter.

When asked to confirm the properties configuration, press enter.

Note that we already updated From the top level project directory (the one with the pom.xml file ) build the project:

Code Block
$ mvn clean install

Assuming that you have ONOS running on your local machine, you can install the app from the command line:

Code Block
$ onos-app localhost install! target/meowster-app-1.0-SNAPSHOT.oar

After refreshing the GUI in your web browser, the navigation menu should have an additional entry:

Image Removed

Clicking on this item should navigate to the injected Sample View:

Image Removed

Selecting a row in the table should display a "details" panel for that item:

Image Removed

The row can be de-selected either by clicking on it again, or by pressing the ESC key.

Pressing the slash ( / ) or backslash ( \ ) key will display the "Quick Help" panel. Press Esc to dismiss:

Image Removed

 

The following sections describe the template files, hopefully providing sufficient context to enable adaptation of the code to implement the needs of your application.

Tabular Views in Brief

in the previous tutorial, it should be good to go as is.

New Files in the Project Structure

You should see a number of new files added to the project:

Image Added

Specifically:

  • New Java classes AppUiTableComponent and AppUiTableMessageHandler
  • New directory ~/resources/app/view/sampleTable, containing:
    • sampleTable.css
    • sampleTable.html
    • sampleTable.js
  • New directory ~/resources/sampleTable, containing:
    • css.html
    • js.html


Building and Installing the App

From the top level project directory (the one with the pom.xml file) build the project:

Code Block
$ cd meowster-sample
$ mvn clean install

 

Assuming that you have ONOS running on your local machine, you can install the app from the command line:

Code Block
$ onos-app localhost install! target/meowster-sample-1.0-SNAPSHOT.oar
Note

If you still have the app installed from the custom view tutorial, you can "reinstall" instead:

  $ onos-app localhost reinstall! target/meowster-sample-1.0-SNAPSHOT.oar

 

After refreshing the GUI in your web browser, the navigation menu should have an additional entry:

Image Added

Clicking on this item should navigate to the injected Sample Table view:

Image Added

Selecting a row in the table should display a "details" panel for that item:

Image Added

The row can be de-selected either by clicking on it again, or by pressing the ESC key.

Pressing the slash ( / ) or backslash ( \ ) key will display the "Quick Help" panel. Press Esc to dismiss:

Image Added

Tabular Views in Brief

To create a tabular view requires both client-side and server-side resources; the client-side To create a tabular view requires both client-side and server-side resources; the client-side consists of HTML, JavaScript, and CSS files, and the server-side consists of Java classes.

  • The HTML file defines the structure of the table view, and indicates to Angular where directives (behaviors) need to be injected.
  • The JavaScript file creates the Angular controller for the view, delegates to the TableBuilderService to build the table, and defines a directive for populating the details panel.
  • The CSS file defines custom styling, if required.
  • The server-side Java code receives :
    • registers the view with the GUI framework
    •  receives requests from the client, fetches the data, formats, sorts and sends
    back
    • the information back to the client.

 

Description of Template Files - Server Side

These files are under the directory ~/src/main/java/org/meowster/app.

Info

The exact path depends on the groupId (also used as the Java package) specified when the application was built with onos-create-app.

 

The descriptions for both AppComponent and AppUiComponent remain the same as in the Custom View tutorial.

AppUiMessageHandler

This class extends UiMessageHandler to implement code that handles events from the (client-side) sample application view. Salient features of note:

This section describes the additional Java files generated by the uitab archetype.

Note

To be able to use the archetype overlay mechanism such that we can add the custom, table, and topology-overlay samples incrementally, we actually create three separate UiExtension instances and register them with the UiExtensionService individually. If your ONOS application was indeed creating multiple views, it should define a single UiExtension instance and declare each of the views and message handlers in one place. See UiExtensionManager.createCoreExtension() for an example of how to do this. Also see the sample application onos-app-uiref.

 

AppUiTableComponent

This is the base class for UI functionality. Things to note:

 

(1) Reference to the UiExtensionService:(1) implement createRequestHandlers() to provide request handler implementations for specific event types from our view.

Code Block
languagejava
@Override
protected Collection<RequestHandler> createRequestHandlers() {
    return ImmutableSet.of(
            new SampleDataRequestHandler(),
            new SampleDetailRequestHandler()
    );
}

 

@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected UiExtensionService uiExtensionService;

Provides access to the UI Extension Service, so that we can register our "view".

 

(2) List of application view descriptors, defining which categories the views appear under in the GUI navigation pane, the internal identifiers for the views, and the corresponding display text(2) define SampleDataRequestHandler class to handle "sampleDataRequest" events from the client. Note that this class extends TableRequestHandler, which implements most of the functionality required to support the table data model:

Code Block
languagejava
private static final String SAMPLEVIEW_DATA_REQID = "sampleDataRequestsampleTable";
private static final String SAMPLEVIEW_DATA_RESPTEXT = "sampleDataResponseSample Table";
...
private static final StringList<UiView> SAMPLESuiViews = "samples";

... 
 
private final class SampleDataRequestHandler extends TableRequestHandler {
    private SampleDataRequestHandler() {
        super(SAMPLE_DATA_REQ, SAMPLE_DATA_RESP, SAMPLES);
    }
    ...
}

Note the call to the super-constructor, which takes three arguments:

  1. request event identifier
  2. response event identifier
  3. "root" tag for data in response payload

To simplify coding (on the client side) the following convention is used for naming these entities:

  • For a given table view, the table is identified by a "tag" (in this example, that tag is "sample")
    • request event identifier is derived as: <tag> + "DataRequest"

    • response event identifier is derived as: <tag> + "DataResponse"
    • "root" tag is derived as: <tag> + "s"

 

(2a) optionally override defaultColumnId():

Code Block
languagejava
// if necessary, override defaultColumnId() -- if it isn't "id"

Typically, table rows have a unique value (row key) to identify the row (for example, in the Devices table it is the value of the Device.id() property). The default identifier for the column holding the row key is "id". If you want to use a different column identifier for the row key, your class should override defaultColumnId(). For example:

Code Block
languagejava
private static final String MAC = "mac";
 
...
 
@Override
protected String defaultColumnId() {
    return MAC;
}

The sample table uses the default column identifier of "id", so can rely on the default implementation and does not need to override the method.

 

(2b) define column identifiers:

Code Block
languagejava
private static final String ID = "id";
private static final String LABEL = "label";
private static final String CODE = "code";

private static final String[] COLUMN_IDS = { ID, LABEL, CODE };

...
 
@Override
protected String[] getColumnIds() {
    return COLUMN_IDS;
}

Note that the column identifiers defined here must match the identifiers specified in the HTML snippet for the view (see sample.html below). 

 

(2c) optionally override createTableModel() to specify custom cell formatters / comparators. 

Code Block
languagejava
// if required, override createTableModel() to set column formatters / comparators
ImmutableList.of(
        new UiView(UiView.Category.OTHER, VIEW_ID, VIEW_TEXT)
);

(Note that this extension in our application only contributes a single view; see note above.)

 

(3) Declaration of a UiMessageHandlerFactory to generate message handlers on demand. The example factory generates a single handler each time, AppUiTableMessageHandler, described below:

Code Block
languagejava
private final UiMessageHandlerFactory messageHandlerFactory =
        () -> ImmutableList.of(
                new AppUiTableMessageHandler()
        );

Generally, there should be one message handler for each contributed view.

 

(4) Declaration of a UiExtension, configured with the previously declared UI view descriptors and message handler factory:

Code Block
languagejava
protected UiExtension extension =
        new UiExtension.Builder(getClass().getClassLoader(), uiViews)
                .resourcePath(VIEW_ID)
                .messageHandlerFactory(messageHandlerFactory)
                .build();

Note that in this case, (as opposed to the Custom View sample code), we also declare a "resource path" (relative to the ~/src/main/resources directory) using the view ID as the subdirectory name. This tells the extension service that the glue files (see later) are located at ~/src/main/resources/sampleTable/*.html.

 

(5) Activation and deactivation callbacks that register and unregister the UI extension at the appropriate timesThe following example sets both a formatter and a comparator for the "code" column:

Code Block
languagejava
@Override@Activate
protected TableModelvoid createTableModelactivate() {
    TableModel tm = super.createTableModel(uiExtensionService.register(extension);
    tmlog.setFormatter(CODE, CodeFormatter.INSTANCE);info("Started");
}

@Deactivate
protected void deactivate() {
    tmuiExtensionService.setComparator(CODE, CodeComparator.INSTANCEunregister(extension);
    return tmlog.info("Stopped");
}
Info

See additional details about table models, formatters, and comparators.

The sample table relies on the default formatter and comparator, and so does not need to override the method.

 

 

AppUiTableMessageHandler

This class extends UiMessageHandler to implement code that handles events from the (client-side) sample table view. Salient features of note:

(1) implement createRequestHandlers() to provide request handler implementations for specific event types from our view.(2d) implement populateTable() to add rows to the supplied table model:

Code Block
languagejava
@Override
protected voidCollection<RequestHandler> populateTable(TableModel tm, ObjectNode payloadcreateRequestHandlers() {
    //return ImmutableSet...of(
     List<Item> items = getItems();
    fornew SampleTableDataRequestHandler(Item item: items) {
),
            new populateRow(tm.addRow(), item);SampleTableDetailRequestHandler()
    });
}
 
private void populateRow(TableModel.Row row, Item item) {
    row.cell(ID, item.id())
        .cell(LABEL, item.label())

 

(2) define SampleTableDataRequestHandler class to handle "sampleTableDataRequest" events from the client. Note that this class extends TableRequestHandler, which implements most of the functionality required to support the table data model:

Code Block
languagejava
private static final String SAMPLE_TABLE_DATA_REQ = "sampleTableDataRequest";
private static final String SAMPLE_TABLE_DATA_RESP = "sampleTableDataResponse";
private static final String SAMPLE_TABLES = "sampleTables";
... 
 
private final class SampleTableDataRequestHandler extends TableRequestHandler {
    private SampleTableDataRequestHandler() {
        .cell(CODE, item.code());
}

Note the payload parameter; this allows contextual information to be passed in with the request, if desired.

The sample table uses fake data for demonstration purposes. A more realistic implementation would use one or more services to obtain the required data. The device table, for example, has an implementation something like this:

Code Block
languagejava
collapsetrue
@Override
protected void populateTable(TableModel tm, ObjectNode payload) {
    DeviceService ds = get(DeviceService.class);
    MastershipService ms = get(MastershipService.class);
    for (Device dev : ds.getDevices()) {
        populateRow(tm.addRow(), dev, ds, ms);
    }
}

private void populateRow(TableModel.Row row, Device dev,
                         DeviceService ds, MastershipService ms) {
    DeviceId id = dev.id();
    String protocol = dev.annotations().value(PROTOCOL);

    row.cell(ID, id)
        .cell(MFR, dev.manufacturer())
        .cell(HW, dev.hwVersion())
        .cell(SW, dev.swVersion())
        .cell(PROTOCOL, protocol != null ? protocol : "")
        .cell(NUM_PORTS, ds.getPorts(id).size())
        .cell(MASTER_ID, ms.getMasterFor(id));
    }
}

 

(3) define SampleDetailRequestHandler class to handle "sampleDetailRequest" events from the client. Note that this class extends the base RequestHandler class:

super(SAMPLE_TABLE_DATA_REQ, SAMPLE_TABLE_DATA_RESP, SAMPLE_TABLES);
    }
    ...
}

Note the call to the super-constructor, which takes three arguments:

  1. request event identifier
  2. response event identifier
  3. "root" tag for data in response payload

To simplify coding (on the client side) the following convention is used for naming these entities:

  • For a given table view, the table is identified by a "tag" (in this example, that tag is "sampleTable")
    • request event identifier is derived as: <tag> + "DataRequest"

    • response event identifier is derived as: <tag> + "DataResponse"
    • "root" tag is derived as: <tag> + "s"

 

(2a) optionally override defaultColumnId():

Code Block
languagejava
// if necessary, override defaultColumnId() -- if it isn't "id"

Typically, table rows have a unique value (row key) to identify the row (for example, in the Devices table it is the value of the Device.id() property). The default identifier for the column holding the row key is "id". If you want to use a different column identifier for the row key, your class should override defaultColumnId(). For example:

Code Block
languagejava
collapsetrue
private static final String MAC = "mac";
...
 
@Override
protected String defaultColumnId() {
    return MAC;
}

The sample table uses the default column identifier of "id", so can rely on the default implementation and does not need to override this method.

 

(2b) define column identifiers:

Code Block
languagejava
private static final String ID = "id";
private static final String LABEL = "label";
private static final String CODE = "code";

private static final String[] COLUMN_IDS = { ID, LABEL, CODE };

...
 
@Override
protected String[] getColumnIds() {
    return COLUMN_IDS;
}

Note that the column identifiers defined here must match the identifiers specified in the HTML snippet for the view (see sampleTable.html below). 

 

(2c) optionally override createTableModel() to specify custom cell formatters / comparators. 

Code Block
languagejava
// if required, override createTableModel() to set column formatters / comparators

The following example sets both a formatter and a comparator for the "code" column:

Code Block
languagejava
collapsetrue
@Override
protected TableModel createTableModel() {
    TableModel tm = super.createTableModel();
    tm.setFormatter(CODE, CodeFormatter.INSTANCE);
    tm.setComparator(CODE, CodeComparator.INSTANCE);
    return tm;
}
Info

The above example code assumes that the classes CodeFormatter (implements CellFormatter) and CodeComparator (implements CellComparator) have been written.

See additional details about table models, formatters, and comparators.

Our sample table relies on the default formatter and comparator, and so does not need to override the method.

 

(2d) implement populateTable() to add rows to the supplied table model:

Code Block
languagejava
@Override
protected void populateTable(TableModel tm, ObjectNode payload) {
    // ...
    List<Item> items = getItems();
    for (Item item: items
Code Block
languagejava
private static final String SAMPLE_DETAIL_REQ = "sampleDetailsRequest";

...
 
private final class SampleDetailRequestHandler extends RequestHandler {

    private SampleDetailRequestHandler() {
        super(SAMPLE_DETAIL_REQ);
 populateRow(tm.addRow(), item);
    }
}
 
private void populateRow(TableModel.Row row,  ...
}

 

(3a) implement process(...) to return detail information about the "selected" row:

Item item) {
    row.cell(ID, item.id())
        .cell(LABEL, item.label())
        .cell(CODE, item.code());
}

Note the payload parameter; this allows contextual information to be passed in with the request, if desired.

The sample table uses fake data for demonstration purposes. A more realistic implementation would use one or more services to obtain the required data. The device table, for example, has an implementation something like this:

Code Block
languagejava
collapsetrue
@Override
protected void populateTable(TableModel tm, ObjectNode payload) {
    DeviceService ds = get(DeviceService.class
Code Block
languagejava
private static final String SAMPLE_DETAIL_RESP = "sampleDetailsResponse";
private static final String DETAILS = "details";
...
private static final String COMMENT = "comment";
private static final String RESULT = "result";

...
 
@Override
public void process(long sid, ObjectNode payload) {
    String id = string(payload, ID, "(none)");

    // SomeService ss = get(SomeService.class);
    // Item item = ss.getItemDetails(id)

    // fake data for demonstration purposes...
    Item item = getItem(id);

    ObjectNode rootNode = MAPPER.createObjectNode();
    ObjectNodeMastershipService datams = MAPPER.createObjectNode(get(MastershipService.class);
    for rootNode.set(DETAILS, data);

    if (item == nullDevice dev : ds.getDevices()) {
        rootNodepopulateRow(tm.put(RESULT, "Item with id '" + id + "' not found");addRow(), dev, ds, ms);
    }
}

private void populateRow(TableModel.Row row, Device dev,
        log.warn("attempted to get item detail for id '{}'", id);

    } else {
   DeviceService ds, MastershipService ms)  rootNode.put(RESULT, "Found item with{
    DeviceId id '" + id + "'"= dev.id();

    String protocol =  data.putdev.annotations().value(PROTOCOL);

    row.cell(ID, item.id());
        data.putcell(LABELMFR, itemdev.labelmanufacturer());
        data.putcell(CODEHW, itemdev.codehwVersion());
        data.putcell(COMMENTSW, "Some arbitrary comment");dev.swVersion())
    }

    sendMessage(SAMPLE_DETAIL_RESP, 0, rootNode);
}

The sample code extracts an item identifier (id) from the payload, and uses that to look up the corresponding item. With the data in hand, it then constructs a JSON object node equivalent to the following structure:

Code Block
languagetext
{
  "details": {.cell(PROTOCOL, protocol != null ? protocol : "")
    "id": "item-1",
    "label": "foo",
    "code": 42,.cell(NUM_PORTS, ds.getPorts(id).size())
    "comment": "Some arbitrary comment" 
  },
  "result": "Found item with id 'item-1'"
}

After which, it sends the message on its way by invoking sendMessage(...).

Description of Template Files - Client Side

Note that the directory naming convention must be observed for the files to be placed in the correct location when the archive is built. Since our view has the unique identifier "sample", its client source files should be placed under the directory ~/src/main/resources/app/view/sample.

~/src/main/resources/app/view/sample/
client filesclient files for UI viewsclient files for "sample" view

There are three files here:

  • sample.html
  • sample.js
  • sample.css

Note the convention to name these files using the identifier for the view; in this case "sample".

sample.html

This is an HTML snippet for the sample view, providing the view's structure. Note that this HTML markup is injected into the Web UI by Angular, to make the view "visible" when the user navigates to it.

Tabular views use a combination of Angular directives and factory services to create the behaviors of the table.

 

Let's describe the different parts of the file in sections...

Outer Wrapper

Code Block
languagexml
<!-- partial HTML -->
<div id="ov-sample">
 
    ...
 
</div>

The outer <div> element defines the contents of your custom "view". It should be given the id of "ov-" + <view identifier>, ("ov" standing for "Onos View"). Thus in this example the id is "ov-sample".

Table View Header <div>

Image Removed

The <div> with class "tabular-header" defines the view's header:

Code Block
languagexml
<div class="tabular-header">
    <h2>Items ({{tableData.length}} total)</h2>
    <div class="ctrl-btns">
        ...
    </div>
</div>

The <h2> heading uses an Angular data binding {{tableData.length}}  to display the number of rows in the table.

The <div> with class "ctrl-btns" is a container for control buttons. Most tables have just an auto-refresh button here, but additional buttons can also be placed at this location.

 .cell(MASTER_ID, ms.getMasterFor(id));
    }
}

 

(3) define SampleTableDetailRequestHandler class to handle "sampleTableDetailRequest" events from the client. Note that this class extends the base RequestHandler class:

Code Block
languagejava
private static final String SAMPLE_TABLE_DETAIL_REQ = "sampleTableDetailsRequest";
...
 
private final class SampleTableDetailRequestHandler extends RequestHandler {
    private SampleTableDetailRequestHandler() {
        super(SAMPLE_TABLE_DETAIL_REQ);
    }
    ...
}

 

(3a) implement process(...) to return detail information about the "selected" row:

Code Block
languagejava
private static final String SAMPLE_TABLE_DETAIL_RESP = "sampleTableDetailsResponse";
private static final String DETAILS = "details";
...
private static final String COMMENT = "comment";
private static final String RESULT = "result";
...
 
@Override
public void process(long sid, ObjectNode payload) {
    String id = string(payload, ID, "(none)");

    // SomeService ss = get(SomeService.class);
    // Item item = ss.getItemDetails(id)

    // fake data for demonstration purposes...
    Item item = getItem(id);

    ObjectNode rootNode = objectNode();
    ObjectNode data = objectNode();
    rootNode.set(DETAILS, data);

    if (item == null) {
        rootNode.put(RESULT, "Item with id '" + id + "' not found");
Code Block
languagexml
 <div class="ctrl-btns">
        <div class="refresh" ng-class="{active: autoRefresh}"
             icon icon-id="refresh" icon-size="36"
             tooltip tt-msg="autoRefreshTip"
        log.warn("attempted to get item  ng-click="toggleRefresh()"></div>
 </div>
Info

See the tablular view directives page for more details about the directives used to define the refresh button.

 

Main Table <div>

The <div> with class "summary-list" defines the actual table.

Code Block
<div class="summary-list" onos-table-resize>

    ...
 
</div>

The onos-table-resize directive dynamically resizes the table to take up the size of the window and to have a scrolling inner body. The column widths are also dynamically adjusted.

Table Header <div>

Image Removed

The <div> with class "table-header" provides a fixed header (i.e. it does not scroll with the table contents), with "click-to-sort" actions on the column labels.

Code Block
languagexml
<div class="table-header" onos-sortable-header>
    <table>
        <tr>detail for id '{}'", id);

    } else {
        rootNode.put(RESULT, "Found item with id '" + id + "'");

        data.put(ID, item.id());
        data.put(LABEL, item.label());
        data.put(CODE, item.code());
        data.put(COMMENT, "Some arbitrary  <td colId="id" sortable>Item ID </td>comment");
    }

    sendMessage(SAMPLE_TABLE_DETAIL_RESP,    <td colId="label" sortable>Label </td>
            <td colId="code" sortable>Code </td>
        </tr>
    </table>
</div>

Info

See the tabular view directives page for more information on the onos-sortable-header and sortable directives.

The colId attributes of each column header cell should match the column identifier values defined in the AppUiMessageHandler class (see above).

Table Body <div>

Image Removed

The <div> with class "table-body" provides a template row for Angular to use to format and populate the table data.

Code Block
languagexml
<div class="table-body">
    <table>

        ...
 
    </table>
</div>

There are two <tr> elements defined in the inner <table> element:

The first is displayed only if there is no table data (i.e. no rows to display). The colspan value should equal the number of columns in the table, so that the single cell spans the whole table width:

Code Block
languagexml
<tr ng-if="!tableData.length" class="no-data">
    <td colspan="3">
        No Items found
    </td>
</tr>

The second is used as a template to stamp out rows; one per data item:

Code Block
<tr ng-repeat="item in tableData track by $index"
    ng-click="selectCallback($event, item)"
    ng-class="{selected: item.id === selId}">
    <td>{{item.id}}</td>
    <td>{{item.label}}</td>
    <td>{{item.code}}</td>
</tr>

The ng-repeat directive tells angular to repeat this structure for each item in the tableData array.

The ng-click directive binds the click handler to the selectCallback() function (defined in the view controller).

The ng-class directive sets the "selected" class on the element if needed (to highlight the row).

Note the use of Angular data-binding (double braces {{ ... }} ) to place the values in the cells.

Details Panel Directive

Finally, a directive is defined to house the "fly-in" details panel, in which to display data about a selected item.

Code Block
languagexml
<ov-sample-item-details-panel></ov-sample-item-details-panel>

Following the naming convention, the prefix to this directive is ov-sample-. More details on what this directive does is shown below.

sample.js

This file defines the view controller, invokes the Table Builder service to do the grunt work in creating the table, and defines a directive to drive the "fly-in" details panel.

Again, we will define the different parts of the file in sections...

Outer Wrapper

An anonymous function invocation is used to wrap the contents of the file, to provide private scope (keeping our variables and functions out of the global scope):

 

Code Block
languagejs
// js for sample app view
(function () {
    'use strict';
 
    ...
 
}());

Variables

Variables are declared to hold injected references (console logger, scope, services), and configuration "constants":

Code Block
languagejs
// injected refs
var $log, $scope, fs, wss, ps;

// constants
var detailsReq = 'sampleDetailsRequest',
    detailsResp = 'sampleDetailsResponse',
    pName = 'ov-sample-item-details-panel',

    propOrder = ['id', 'label', 'code'],
    friendlyProps = ['Item ID', 'Item Label', 'Special Code'];

 

Helper Functions

Next come helper functions that we will examine in more detail in a bit:

Code Block
languagejs
function addProp(tbody, index, value) { 
    ...
}
 
function populatePanel(panel) { 
    ... 
}

 

Detail Response Callback

Next we define the callback function to be invoked when a "sampleDetailsResponse" event arrives with details about a selected item:

Code Block
function respDetailsCb(data) {
    $scope.panelDetails = data.details;
    $scope.$apply();
}

It's job is to take the data from the payload of the event (passed in as the data parameter) and store it in our scope, so that it is available for the details panel directive to reference.

Defining the Controller

Next our view controller is defined. It might help to break this into pieces too...

Code Block
languagejs
angular.module('ovSample', [])
    .controller('OvSampleCtrl'
    ['$log', '$scope', 'TableBuilderService',
        'FnService', 'WebSocketService',
 
        function (_$log_, _$scope_, tbs, _fs_, _wss_) {
            ...
        }])

The first line here gets a reference to the "ovSample" module. Again, this is the naming convention in play; the module name should start with "ov" (lowercase) followed by the identifier for our view, first letter capitalised.

The controller() function is invoked on the module to define our controller. The first argument – "OvSampleCtrl" – is the name of our controller, as registered with angular. Once again, the naming convention is to start with "Ov", followed by the identifier for our view (first letter capitalized), followed by "Ctrl". Case is important. The second argument is an array...

All the elements of the array (except the last) are the names of services to be injected into our controller at run time. Angular uses these to bind the actual services to the specified parameters of the controller function (the last item in the array).

 

Inside our controller function, we start by saving injected references inside our closure, so that they are available to other functions. We also initialize our state:

Code Block
languagejs
$log = _$log_;
$scope = _$scope_;
fs = _fs_;
wss = _wss_;

var handlers = {};
$scope.panelDetails = {};

Next up, we need to bind our event handler function to the web socket service, so that when a "sampleDetailsResponse" event comes in from the server, it gets routed to us:

Code Block
languagejs
// details response handler
handlers[detailsResp] = respDetailsCb;
wss.bindHandlers(handlers);
Info

See the Web Socket Service for further details on event handler binding.

Note that we do not need to worry about handling the basic table event response ("sampleDataResponse" in our case), as that is handled behind the scenes by the table builder, which we will see shortly.

 

0, rootNode);
}

The sample code extracts an item identifier (id) from the payload, and uses that to look up the corresponding item. With the data in hand, it then constructs a JSON object node equivalent to the following structure:

Code Block
languagetext
{
  "details": {
    "id": "item-1",
    "label": "foo",
    "code": 42,
    "comment": "Some arbitrary comment" 
  },
  "result": "Found item with id 'item-1'"
}

After which, it sends the message on its way by invoking sendMessage(...).

Description of Template Files - Client Side

Note that the directory naming convention must be observed for the files to be placed in the correct location when the archive is built. Since our view has the unique identifier "sampleTable", its client source files should be placed under the directory ~/src/main/resources/app/view/sampleTable.

~/src/main/resources/app/view/sampleTable/
client filesclient files for UI viewsclient files for "sampleTable" view

There are three files here:

  • sampleTable.html
  • sampleTable.js
  • sampleTable.css

Note the convention to name these files using the identifier for the view; in this case "sampleTable".

sampleTable.html

This is an HTML snippet for the sample table view, providing the view's structure. Note that this HTML markup is injected into the Web UI by Angular, to make the view "visible" when the user navigates to it.

Tabular views use a combination of Angular directives and factory services to create the behaviors of the table.

 

Let's describe the different parts of the file in sections...

Outer Wrapper

Code Block
languagexml
<!-- partial HTML -->
<div id="ov-sample-table">
    ...
</div>

The outer <div> element defines the contents of your custom "view". It should be given the id of "ov-" + <view identifier>, ("ov" standing for "Onos View"). Thus in this example the id is "ov-sample-table".

Table View Header <div>

Image Added

The <div> with class "tabular-header" defines the view's header:

Code Block
languagexml
<div class="tabular-header">
    <h2>Items ({{tableData.length}} total)</h2>
    <div class="ctrl-btns">
        ...
    </div>
</div>

The <h2> heading uses an Angular data binding {{tableData.length}}  to display the number of rows in the table.

The <div> with class "ctrl-btns" is a container for control buttons. Most tables have just an auto-refresh button here, but additional buttons can also be placed at this location.

Code Block
languagexml
 <div class="ctrl-btns">
        <div class="refresh" ng-class="{active: autoRefresh}"
             icon icon-id="refresh" icon-size="36"
             tooltip tt-msg="autoRefreshTip"
             ng-click="toggleRefresh()"></div>
 </div>
Info

See the tablular view directives page for more details about the directives used to define the refresh button.

 

Main Table <div>

The <div> with class "summary-list" defines the actual table.

Code Block
languagexml
<div class="summary-list" onos-table-resize>
    ...
</div>

The onos-table-resize directive dynamically resizes the table to take up the size of the window and to have a scrolling inner body. The column widths are also dynamically adjusted.

Table Header <div>

Image Added

The <div> with class "table-header" provides a fixed header (i.e. it does not scroll with the table contents), with "click-to-sort" actions on the column labels.

Code Block
languagexml
<div class="table-header" onos-sortable-header>
    <table>
        <tr>
            <td colId="id" sortable>Item ID </td>
            <td colId="label" sortable>Label </td>
            <td colId="code" sortable>Code </td>
        </tr>
    </table>
</div>
Info

See the tabular view directives page for more information on the onos-sortable-header and sortable directives.

Note

The colId attributes of each column header cell must match the column identifier values defined in the AppUiTableMessageHandler class (see above).

Table Body <div>

Image Added

The <div> with class "table-body" provides a template row for Angular to use to format and populate the table data:

Code Block
languagexml
<div class="table-body">
    <table>
        ...
    </table>
</div>

There are two <tr> elements defined in the inner <table> element:

The first is displayed only if there is no table data (i.e. no rows to display). The colspan value should equal the number of columns in the table, so that the single cell spans the whole table width:

Code Block
languagexml
<tr ng-if="!tableData.length" class="no-data">
    <td colspan="3">
        No Items found
    </td>
</tr>

The second is used as a template to stamp out rows; one per data item:

Code Block
languagexml
<tr ng-repeat="item in tableData track by $index"
    ng-click="selectCallback($event, item)"
    ng-class="{selected: item.id === selId}">
    <td>{{item.id}}</td>
    <td>{{item.label}}</td>
    <td>{{item.code}}</td>
</tr>

The ng-repeat directive tells angular to repeat this structure for each item in the tableData array.

The ng-click directive binds the click handler to the selectCallback() function (defined in the view controller).

The ng-class directive sets the "selected" class on the element if needed (to highlight the row).

Note the use of Angular data-binding (double braces {{ ... }} ) to place the values in the cells.

Details Panel Directive

Finally, a directive is defined to house the "fly-in" details panel, in which to display data about a selected item.

Code Block
languagexml
<ov-sample-table-item-details-panel></ov-sample-table-item-details-panel>

Following the naming convention, the prefix to this directive is ov-sample-table-. More details on what this directive does is shown below.

sampleTable.js

This file defines the view controller, invokes the Table Builder Service to do the grunt work in creating the table, and defines a directive to drive the "fly-in" details panel.

Again, we will define the different parts of the file in sections...

Outer Wrapper

An anonymous function invocation is used to wrap the contents of the file, to provide private scope (keeping our variables and functions out of the global scope)Next we define the row selection callback function; this is invoked when the user clicks on a row in the table:

Code Block
languagejs
// custom selection callback
js for sample app view
(function selCb($event, row) {
    if ($scope.selId) {'use strict';
        wss.sendEvent(detailsReq, { id: row.id });
    } else {...
}());

Variables

Variables are declared to hold injected references (console logger, scope, services), and configuration "constants":

Code Block
languagejs
// injected refs
var $log, $scope, fs, wss;

// constants
var detailsReq = 'sampleTableDetailsRequest',
    detailsResp =   $scope.hidePanel();
    }
    $log.debug('Got a click on:', row);
}

Behind the scenes, the table builder code will set the selId property on our scope to the identity (value of the "id" column) of the selected row (or null if no row is selected). It then invokes our callback function, passing it a reference to the event object, as well as the data structure for the row in question.

Our function uses the web socket service (wss) to send the details request event to the server, embedding the item identifier in the event payload.

In the case where there is no longer an item selected, the behavior is to hide the details panel.

 

Now we delegate the bulk of the work to the table builder service, providing it with the necessary references:

Code Block
languagejs
// TableBuilderService creating a table for us
tbs.buildTable({
    scope: $scope,
    tag: 'sample',
    selCb: selCb
}); 

The buildTable() function takes an options object as its single argument, to configure the table. The properties of the object are:

  • scope: a reference to our view's $scope (as injected into our controller by Angular)
  • tag: our table's "tag" used to derive the request and response event names and the "root" tag for the payload data (as described above; see SampleDataRequestHandler)
  • selCb: a reference to our row selection callback, described above
Info

See the Table Builder Service for more details, included additional parameters not used here.

 

Finally we register a cleanup function with Angular, to be invoked when our view is destroyed (when the user navigates away to some other view):

Code Block
languagejs
// cleanup
$scope.$on('$destroy', function () {
    wss.unbindHandlers(handlers);
}); 

Here, we simply need to unbind our event handlers from the websocket service.

Defining the Details Panel Directive

Chained onto the .controller() function call is a call to .directive() to define our details panel directive. Again, let's break the code down into chunks...

Code Block
languagejs
.directive('ovSampleItemDetailsPanel', ['PanelService', 'KeyService',
    function (ps, ks) {
        return {
            ...
        };
    }]);

The two arguments to directive() are:

  • the name of our directive
  • an array

The name, once again, follows the naming convention of starting with our prefix, "ovSample". The whole name – "ovSampleItemDetailsPanel" – is understood by Angular to relate to the HTML element <ov-sample-item-details-panel> that we saw at the bottom of the HTML file.

All elements of the array (except the last) are the names of services to inject into the directive function (the last element of the array). In this case we are injecting references to the Panel Service and the Key Service.

 

Our function returns an object to allow Angular to configure our directive:

'sampleTableDetailsResponse',
    pName = 'ov-sample-table-item-details-panel',

    propOrder = ['id', 'label', 'code'],
    friendlyProps = ['Item ID', 'Item Label', 'Special Code'];

 

Helper Functions

Next come helper functions that we will examine in more detail in a bit:

Code Block
languagejs
function addProp(tbody, index, value) { 
    ...
}
 
function populatePanel(panel) { 
    ... 
}

 

Detail Response Callback

Next we define the callback function to be invoked when a "sampleTableDetailsResponse" event arrives with details about a selected item:

Code Block
languagejs
function respDetailsCb(data) {
    $scope.panelDetails = data.details;
    $scope.$apply();
}

It's job is to take the data from the payload of the event (passed in as the data parameter) and store it in our scope, so that it is available for the details panel directive to reference.

Defining the Controller

Next our view controller is defined. It might help to break this into pieces too...

Code Block
languagejs
angular.module('ovSampleTable', [])
    .controller('OvSampleTableCtrl'
    ['$log', '$scope', 'TableBuilderService',
        'FnService', 'WebSocketService',
 
        function (_$log_, _$scope_, tbs, _fs_, _wss_) {
            ...
        }])

The first line here registers our "ovSampleTable" module with angular, (the empty array states that our module is not dependent on any other module). Again, note the naming convention in play; the module name should start with "ov" (lowercase) followed by the identifier for our view, in continuing camel-case.

The controller() function is invoked on the module to define our controller. The first argument – "OvSampleTableCtrl" – is the name of our controller. Once again, the naming convention is to start with "Ov" (uppercase 'O') followed by the identifier for our view (continuing camel-case), followed by "Ctrl". The second argument is an array...

All the elements of the array (except the last) are the names of services to be injected into our controller function at run time. Angular uses these to bind the actual services to the specified parameters of the controller function (the last item in the array).

 

Inside our controller function, we start by saving injected references inside our closure, so that they are available to other functions:

Code Block
languagejs
$log = _$log_;
$scope = _$scope_;
fs = _fs_;
wss = _wss_;

 

We also initialize our state; our map of event handlers, and the cached data for the selected item (initially empty):

Code Block
languagejs
var handlers = {};
$scope.panelDetails = {};

 

Next, we need to tell the WebSocketService which callback function to invoke when a "sampleTableDetailsResponse" event arrives from the server:

Code Block
languagejs
handlers[detailsResp] = respDetailsCb;
wss.bindHandlers(handlers);
Info

See the Web Socket Service for further details on event handler binding.

Note that we do not need to worry about handling the basic table event response ("sampleTableDataResponse" in our case), as that is handled behind the scenes by the table builder, which we will see shortly.

 

Next we define the row selection callback function; this is invoked when the user clicks on a row in the table:

Code Block
languagejs
function selCb($event, row) {
    if ($scope.selId) {
        wss.sendEvent(detailsReq, { id: row.id });
    } else 
Code Block
languagejs
return {
    restrict: 'E', 
    link: function (scope, element, attrs) {
        ...$scope.hidePanel();
    }
    $log.debug('Got a click on:', row);
};

The restrict property is set to "E" to tell angular that this directive is an element.

The link function is invoked when Angular parses the HTML document and finds the <ov-sample-item-details-panel> element. Our function sets up the "floating panel" behaviors as follows:

Behind the scenes, the table builder code will set the selId property on our scope to the identity (value of the "id" column) of the selected row (or null if no row is selected). It then invokes our callback function, passing it a reference to the event object, as well as the data structure for the row in question.

Our function uses the WebSocketService (wss) to send the details request event to the server, embedding the item identifier in the event payload.

In the case where there is no longer an item selected, the behavior is to hide the details panel instead.

 

Now we delegate the bulk of the work to the TableBuilderService (tbs), providing it with the necessary references:

Code Block
languagejs
tbs.buildTable(
Code Block
languagejs
var panel = ps.createPanel(pName, {
    widthscope: 200$scope,
    margintag: 20'sampleTable',
    hideMarginselCb: 0selCb
});
panel.hide();
scope.hidePanel = function () { panel.hide(); };

First, use the panel service to create the panel, and start with the panel hidden. Also store a hidePanel() function on our scope which we can invoke later.

Next we define a callback to hide the panel if it is visible; to be bound to the ESC keystroke:

 

The buildTable() function takes an options object as its single argument, to configure the table. The properties of the object used in our sample table are:

  • scope: a reference to our view's $scope (as injected into our controller by Angular)
  • tag: our table's "tag" used to derive the request and response event names and the "root" tag for the payload data (as described above; see SampleTableDataRequestHandler)
  • selCb: a reference to our row selection callback, described above
Info

See the Table Builder Service for more details, included additional parameters not used here.

 

When our view is destroyed (when the user navigates to some other view), we need to do some cleanup:

Code Block
languagejs
$scope.$on('$destroy', function ()
Code Block
languagejs
function closePanel() {
    if (panel.isVisible()) {
        $scope.selId = nullwss.unbindHandlers(handlers);
        panel.hide();
        return true;
    }
    return false;
} 

Note that escape key handlers should return true if they "consume" the event, or false otherwise.

Info

See the Panel Service for more details on creating and using "floating" panels.

 

$log.log('OvSampleTableCtrl has been destroyed');
}); 

We unbind our event handlers, and log a message to the console.

 

The last thing the controller does when it is initialized is to log to the consoleNow we use the key service to bind our callback to the ESC key, and also provide hints for the Quick Help panel:

Code Block
languagejs
// create key bindings to handle panel
ks.keyBindings({
    esc: [closePanel, 'Close the details panel'],
    _helpFormat: ['esc']
});
ks.gestureNotes([
    ['click', 'Select a row to show item details']
]); 
Info

See the Key Service for more details on binding keys / configuring quick help.

 

Next, we set up a "watch" function to be triggered any time the panelDetails property on our scope is changed. If the object is non-empty, the function clears then populates the panel with data, and displays it.

$log.log('OvSampleTableCtrl has been created');

 

Defining the Details Panel Directive

Chained onto the .controller() function call is a call to .directive(...) to define our details panel directive. Again, let's break the code down into chunks...

Code Block
languagejs
.directive('ovSampleTableItemDetailsPanel', ['PanelService', 'KeyService',
    function (ps, ks) {
    return {
        ...
    };
}]);

The two arguments to directive() are:

  • the name of our directive
  • an array

The name, once again, follows the naming convention of starting with our prefix, "ovSampleTable". The whole name – "ovSampleTableItemDetailsPanel" – is understood by Angular to relate to the HTML element <ov-sample-table-item-details-panel> that we saw at the bottom of the HTML file.

All elements of the array (except the last) are the names of services to inject into the directive function (the last element of the array). In this case we are injecting references to the Panel Service and the Key Service.

 

Our function returns an object to allow Angular to configure our directive:

Code Block
languagejs
return {
    restrict: 'E', 
    link: function (scope, element, attrs)
Code Block
languagejs
// update the panel's contents when the data is changed
scope.$watch('panelDetails', function () {
    if (!fs.isEmptyObject(scope.panelDetails)) {
        panel.empty();...
        populatePanel(panel);
        panel.show();
}    }
}); 

Now would be a good time to revisit those helper functions that we glossed over earlier. First, populatePanel()...

The restrict property is set to "E" to tell angular that this directive is an element.

The link function is invoked when Angular parses the HTML document and finds the <ov-sample-table-item-details-panel> element. Our function sets up the "floating panel" behaviors as follows:

Code Block
languagejs
function populatePanel(panel)var panel = ps.createPanel(pName, {
    var title = panel.append('h3')width: 200,
    margin: 20,
   tbody = hideMargin: 0
});
panel.appendhide('table').append('tbody');

    title.text('Item Details');

    propOrder.forEach(function (prop, i) {
        addProp(tbody, i, $scope.panelDetails[prop]);
;
scope.hidePanel = function () { panel.hide(); };

First, use the PanelService to create the panel, and start with the panel hidden. Also store a hidePanel() function on our scope which we can invoke later.

Next we define a callback to hide the panel if it is visible; to be bound to the ESC keystroke:

Code Block
languagejs
function closePanel() {
    if (panel.isVisible()) {
        $scope.selId = null    });

    panel.append('hr');
    panel.append('h4').text('Comments');
        panel.appendhide('p').text($scope.panelDetails.comment));
        return true;
    }
    return false;
}

This function is given a reference to the panel instance. It uses that API to build up the panel contents from the data stored in the panelDetails property of our scope.

Recall, propOrder was defined as an array (at the top of our script) listing the order in which the properties of the data should be added to the panel.

 

Note that escape key handlers should return true if they "consume" the event, or false otherwise.

Info

See the Panel Service for more details on creating and using "floating" panels.

 

Now we use the key service to bind our callback to the ESC key, and also provide hints for the Quick Help The addProp() function adds a property line item to the panel:

Code Block
languagejs
function addProp(tbody, index, value) // create key bindings to handle panel
ks.keyBindings({
    var tr = tbody.append('tr');

    function addCell(cls, txt) {esc: [closePanel, 'Close the details panel'],
        tr.append('td').attr('class', cls).html(txt);_helpFormat: ['esc']
});
ks.gestureNotes([
    }
    addCell('label', friendlyProps[index] + ' :');
    addCell('value', value);
} 

Recall, friendlyProps was defined as an array listing the "human readable" labels for each of the properties.

 

['click', 'Select a row to show item details']
]); 
Info

See the Key Service for more details on binding keys / configuring quick help.

 

Next, we set up a "watch" function to be triggered any time the panelDetails property on our scope is changed. If the object is non-empty, the function clears then populates the panel with data, and displays it.And finally... add a cleanup function for when our scope is destroyed (user navigates away from our view):

Code Block
languagejs
 // cleanup on destroyed scope update the panel's contents when the data is changed
scope.$on$watch('$destroypanelDetails', function () {
    ks.unbindKeys();
if (!fs.isEmptyObject(scope.panelDetails)) {
        pspanel.destroyPanelempty(pName);
});

Unbind our ESC key handler and gesture notes and destroy the floating panel instance.

sample.css

        populatePanel(panel);
        panel.show();
    }
}); 

Now would be a good time to revisit those helper functions we glossed over earlier. First, populatePanel()..The last file in our client-side trio is the stylesheet for the sample view.

Code Block
languagecss
linenumberstrue
collapsetrue
/* css for sample app view */

#ov-sample h2 {
    display: inline-block;
}

/* Panel Styling */
#ov-sample-item-details-panel.floatpanel {
    position: absolute;
    top: 115px;
}

.light #ov-sample-item-details-panel.floatpanel {
    background-color: rgb(229, 234, 237);
}
.dark #ov-sample-item-details-panel.floatpanel {
    background-color: #3A4042;
}

#ov-sample-item-details-panel h3 {
    margin: 0;
    font-size: large;
}

#ov-sample-item-details-panel h4 {
    margin: 0;
}

#ov-sample-item-details-panel td {
    padding: 5px;
}
#ov-sample-item-details-panel td.label {
    font-style: italic;
    opacity: 0.8;
} 

It should be fairly self-explanatory. However, note the use of the .light and .dark classes (lines 13-18) to select an appropriate color for the panel for each of the GUI's themes.

js
function populatePanel(panel) {
    var title = panel.append('h3'),
        tbody = panel.append('table').append('tbody');

    title.text('Item Details');

    propOrder.forEach(function (prop, i) {
        addProp(tbody, i, $scope.panelDetails[prop]);
    });

    panel.append('hr');
    panel.append('h4').text('Comments');
    panel.append('p').text($scope.panelDetails.comment);
}

This function is given a reference to the panel instance. It uses that API to build up the panel contents from the data stored in the panelDetails property of our scope.

Recall, propOrder was defined as an array (at the top of our script) listing the order in which the properties of the data should be added to the panel.

The addProp() function adds a property line item to the panel:

Code Block
languagejs
function addProp(tbody, index, value) {
    var tr = tbody.append('tr');

    function addCell(cls, txt) {
        tr.append('td').attr('class', cls).html(txt);
    }
    addCell('label', friendlyProps[index] + ' :');
    addCell('value', value);
} 

Recall, friendlyProps was defined as an array listing the "human readable" labels for each of the properties.

 

And finally... add a cleanup function for when our scope is destroyed (user navigates away from our view):

Code Block
languagejs
 // cleanup on destroyed scope
scope.$on('$destroy', function () {
    ks.unbindKeys();
    ps.destroyPanel(pName);
});

Unbind our ESC key handler and gesture notes and destroy the floating panel instance.

sample.css

The last file in our client-side trio is the stylesheet for the sample view.

Code Block
languagecss
collapsetrue
/* css for sample app view */

#ov-sample-table h2 {
    display: inline-block;
}

/* Panel Styling */
#ov-sample-table-item-details-panel.floatpanel {
    position: absolute;
    top: 115px;
}

.light #ov-sample-table-item-details-panel.floatpanel {
    background-color: rgb(229, 234, 237);
}
.dark #ov-sample-table-item-details-panel.floatpanel {
    background-color: #3A4042;
}

#ov-sample-table-item-details-panel h3 {
    margin: 0;
    font-size: large;
}

#ov-sample-table-item-details-panel h4 {
    margin: 0;
}

#ov-sample-table-item-details-panel td {
    padding: 5px;
}
#ov-sample-table-item-details-panel td.label {
    font-style: italic;
    opacity: 0.8;
} 

It should be fairly self-explanatory. However, note:

  • #ov-sample-table h2 is displayed as inline-block, so that the view title and the control button block are displayed on the same line:

    Image Added

 

  • the use of the .light and .dark classes to select appropriate colors for each of the GUI's themes.

Image Added           Image Added

Glue Files

The final piece to the puzzle are the two "glue" files used to patch references to our client-side source into index.html.

Recall in AppUiTableComponent we defined the resource path when building the UiExtension instance...

Code Block
languagejava
private static final String VIEW_ID = "sampleTable";
...
protected UiExtension extension = 
        new UiExtension.Builder(getClass().getClassLoader(), uiViews)
                .resourcePath(VIEW_ID)
                ...

This tells the framework to look in the directory ~src/main/resources/sampleTable/ for the glue files. (As noted before, we did this so that the three archetype overlays could co-exist.)

css.html

This is a short snippet that is injected into index.html. It contains exactly one line:

Code Block
languagexml
<link rel="stylesheet" href="app/view/sampleTable/sampleTable.css">

js.html

This is a short snippet that is injected into index.html. It contains exactly one line:

Code Block
languagexml
<script src="app/view/sampleTable/sampleTable.js"></script>

 

Summary

Obviously, this sample table doesn't do anything useful, with hard-coded data, but it should serve as a template of how to stitch things together to create your own tabular views. Do remember to follow the naming convention – generally, using your view ID as a prefix to DOM element, event names, and other "public" identifiers.

Have fun creating tables!Image Removed           Image Removed