Overview

Tabular views, unsurprisingly, present data in a tabular form. As an example, here is a screenshot of the hosts view...

Applications can create (and inject into the GUI) their own view(s) of tabular data.

A basic tabular view:

This tutorial will step you through the process of creating an application that does just that - 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:

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

 

(2) Overlay the UI additional components

$ onos-create-app uitab org.meowster.app meowster-app

Building and Installing the App

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

$ mvn clean install

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

$ 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:

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

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

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:

 

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

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.

 

Description of Template Files - Server Side

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

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:

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

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

 

(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:

private static final String SAMPLE_DATA_REQ = "sampleDataRequest";
private static final String SAMPLE_DATA_RESP = "sampleDataResponse";
private static final String SAMPLES = "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:

 

(2a) optionally override defaultColumnId():

// 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:

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:

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. 

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

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

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

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.

 

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

@Override
protected void populateTable(TableModel tm, ObjectNode payload) {
    // ...
    List<Item> items = getItems();
    for (Item item: items) {
        populateRow(tm.addRow(), item);
    }
}
 
private void populateRow(TableModel.Row row, Item item) {
    row.cell(ID, item.id())
        .cell(LABEL, item.label())
        .cell(CODE, item.code());
}

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:

@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:

private static final String SAMPLE_DETAIL_REQ = "sampleDetailsRequest";

...
 
private final class SampleDetailRequestHandler extends RequestHandler {

    private SampleDetailRequestHandler() {
        super(SAMPLE_DETAIL_REQ);
    }

    ...
}

 

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

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();
    ObjectNode data = MAPPER.createObjectNode();
    rootNode.set(DETAILS, data);

    if (item == null) {
        rootNode.put(RESULT, "Item with id '" + id + "' not found");
        log.warn("attempted to get item 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 comment");
    }

    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:

{
  "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 "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:

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...

<!-- 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>

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

<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.

 <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>

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.

<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>

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.

<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>

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 Row Template <div>

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

<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:

<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:

<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.

<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

blah

sample.css

Stylesheet for the sample view. Again, a number of naming conventions are in use here: