Have questions? Stuck? Please check our FAQ for some common questions and answers.

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 28 Next »

Overview

An application can create their own custom views and have them integrated into the ONOS web GUI. This tutorial walks you through the process of developing such a view. A fictitious company, "Meowster, Inc." is used throughout the examples. This tutorial assumes you have created a top level directory and changed into it:

$ mkdir meow
$ cd meow

 

Application Set Up

The quickest way to get started is to use the maven archetypes to create the application source directory structure and fill it with template code. You can get more details here, but to summarize:

(0) Create a working directory

$ mkdir custom
$ cd custom

 

(1) Create the main application

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

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

When asked to confirm the properties configuration, press enter.

groupId: org.meowster.app.custom
artifactId: meowster-custom
version: 1.0-SNAPSHOT
package: org.meowster.app.custom

 

(2) Overlay the UI additional components

$ onos-create-app ui org.meowster.app.custom meowster-custom

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

When asked to confirm the properties configuration, press enter.


(3) Modify the pom.xml file to mark the module as an ONOS app:

$ cd meowster-custom
$ vi pom.xml

(3a) Change the description:

<description>Meowster Sample ONOS Custom-View App</description>

(3b) In the <properties> section, change the app name and origin:

<onos.app.name>org.meowster.app.custom</onos.app.name>
<onos.app.origin>Meowster, Inc.</onos.app.origin>

Everything else in the pom.xml file should be good to go.

Import into IntelliJ, if you wish

You can import the application source into IntelliJ as a new project.

Start by selecting File --> New --> Project from Existing Sources...

Then navigate to the top level pom.xml file:

On the next screen, you might wish to checkmark the following two items:

  • Search for projects recursively
  • Import Maven projects automatically

...since they are not selected by default.

IntelliJ should find the 1.0-SNAPSHOT:

If presented with a choice of Java versions, select 1.8:

Finally, finish with the name of the project:

Once the project loads, you should finally see a directory structure like this:

Building and Installing the App

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

$ mvn clean install

Note that your application is bundled as an .oar (ONOS Application ARchive) file in the target directory, and installed into your local maven repository:

...
[INFO] Installing /Users/simonh/dev/meow/custom/meowster-custom/target/meowster-custom-1.0-SNAPSHOT.oar to 
       /Users/simonh/.m2/repository/org/meowster/app/custom/meowster-custom/1.0-SNAPSHOT/meowster-custom-1.0-SNAPSHOT.oar
...

 

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-custom-1.0-SNAPSHOT.oar

You should see some JSON output that looks something like this:

{"name":"org.meowster.app.custom","id":39,"version":"1.0.SNAPSHOT",
"description":"Meowster Sample ONOS Custom-View App",
"origin":"Meowster, Inc.","permissions":"[]",
"featuresRepo":"mvn:org.meowster.app.custom/meowster-custom/1.0-SNAPSHOT/xml/features",
"features":"[meowster-custom]","state":"ACTIVE"}

 

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

Clicking on the Fetch Data button with the mouse, or pressing the space-bar, will update the view with new data.

Note that pressing slash (/) will display the "Quick Help" panel:

As you will see in the code (in a later section), we created a key-binding with the space-bar (to fetch data); it is thus listed in the quick help panel.

Also of note: pressing the T key will toggle the "theme" between light and dark:

Custom Views in Brief

To create a custom 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 custom view, and indicates to Angular where directives (behaviors) need to be injected.
  • The JavaScript file creates the Angular controller for the view, and defines code that drives the view.
  • The CSS file defines custom styling.
  • The server-side Java code receives requests from the client, fetches (and formats) data, and sends the information back to the client.

 

Description of Template Files - Server Side

This section gives a brief introduction to the generated files. 

AppComponent

This is the base Application class and may be used for non-UI related functionality (not addressed in this tutorial). 

AppUiComponent

This is the base class for UI functionality. The salient features to note are introduced briefly below.

 

(1) Reference to the UiExtensionService:

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

 

(2) List of application view descriptors, defining which category the view appears under in the GUI navigation pane (if not a hidden view), the internal identifier for the view, and the display text for the link:

 // List of application views
private final List<UiView> uiViews = ImmutableList.of(
        new UiView(UiView.Category.OTHER, "sampleCustom", "Sample Custom")
);

 

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

// Factory for UI message handlers
private final UiMessageHandlerFactory messageHandlerFactory =
        () -> ImmutableList.of(
                new AppUiMessageHandler()
        );

 

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

// Application UI extension
protected UiExtension extension =
        new UiExtension.Builder(getClass().getClassLoader(), uiViews)
                .messageHandlerFactory(messageHandlerFactory)
                .build();

 

(5) Activation and deactivation callbacks that register and unregister the UI extension at the appropriate times:

@Activate
protected void activate() {
    uiExtensionService.register(extension);
    log.info("Started");
}

@Deactivate
protected void deactivate() {
    uiExtensionService.unregister(extension);
    log.info("Stopped");
}

AppUiMessageHandler

This class extends UiMessageHandler to implement code to handle events from the (client-side) sample application view.

 

Salient features to 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 SampleCustomDataRequestHandler()
    );
}

In this simple example, we only have to deal with a single event type, but this is where other event type handlers would be declared if our view generated additional events.

 

(2) define SampleCustomDataRequestHandler class to handle "sampleCustomDataRequest" events from the client. 

private static final String SAMPLE_CUSTOM_DATA_REQ = "sampleCustomDataRequest";
...
private final class SampleCustomDataRequestHandler extends RequestHandler {
    private SampleCustomDataRequestHandler() {
        super(SAMPLE_CUSTOM_DATA_REQ);
    }
    ...
}

Note that this class extends RequestHandler, which defines an abstract process() method to be implemented by subclasses.

The constructor takes the name of the event to be handled; at run time, an instance of this handler is bound to the event name, and the process() method invoked each time such an event is received from the client.

 

(3) implement process() method:

private static final String SAMPLE_CUSTOM_DATA_RESP = "sampleCustomDataResponse";

private static final String NUMBER = "number";
private static final String SQUARE = "square";
private static final String CUBE = "cube";
private static final String MESSAGE = "message";
private static final String MSG_FORMAT = "Next incrememt is %d units";

private long someNumber = 1;
private long someIncrement = 1;
...
 
@Override
public void process(long sid, ObjectNode payload) {
    someIncrement++;
    someNumber += someIncrement;
    log.debug("Computing data for {}...", someNumber);

    ObjectNode result = objectNode();
    result.put(NUMBER, someNumber);
    result.put(SQUARE, someNumber * someNumber);
    result.put(CUBE, someNumber * someNumber * someNumber);
    result.put(MESSAGE, String.format(MSG_FORMAT, someIncrement + 1));
    sendMessage(SAMPLE_CUSTOM_DATA_RESP, 0, result);
}

The process method is invoked every time a "sampleCustomDataRequest" event is received from the client. 

The sid parameter (sequence ID) is deprecated.

The payload parameter provides the payload of the request event, but we are not using it in this example

The method creates and populates an ObjectNode instance with data to be (transformed into JSON and) shipped off back to the client.

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 "sampleCustom", its client source files should be placed under the directory ~/src/main/resources/app/view/sampleCustom.

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

There are three files here:

  • sampleCustom.html
  • sampleCustom.js
  • sampleCustom.css

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

sampleCustom.html

This is an HTML snippet for the sample custom 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.

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

Outer Wrapper

<!-- partial HTML -->
<div id="ov-sample-custom">
    ...
</div>

The outer <div> element defines the contents of our custom "view". It should use a dash-delimited "id" of the internal identifier for the view, prefixed with "ov". So, in our example, "sampleCustom" becomes "ov-sample-custom". ("ov" stands for "ONOS view").

Button Panel

We have defined a <div> to create a "button panel" at the top of the view:

<div class="button-panel">
    <div class="my-button" ng-click="getData()">
        Fetch Data
    </div>
</div>

Note that we have used Angular's ng-click attribute in the button <div> to instruct Angular to invoke the getData() function defined on our "scope" (see later), whenever the user clicks the <div>.

Data Panel

We have defined a <div> to create a "data panel" below:

<div class="data-panel">
    <table>
        <tr>
            <td> Number </td>
            <td class="number"> {{data.number}} </td>
        </tr>
        <tr>
            <td> Square </td>
            <td class="number"> {{data.square}} </td>
        </tr>
        <tr>
            <td> Cube </td>
            <td class="number"> {{data.cube}} </td>
        </tr>
    </table>

    <p>
        A message from our sponsors:
    </p>
    <p>
        <span class="quote"> {{data.message}} </span>
    </p>
</div>

Note the use of Angular's double-braces {{ }} for variable substitution. For example, the expression {{data.number}} is replaced by the current value of the number property of the data object stored on our "scope" (see later).

sampleCustom.js

This file defines the view controller, and provides callback functions to drive the view.

Again, we will examine the different parts of the code 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):

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

Variables

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

// injected refs
var $log, $scope, wss, ks;

// constants
var dataReq = 'sampleCustomDataRequest',
    dataResp = 'sampleCustomDataResponse';

Defining the Controller 

We'll skip over the helper functions for the moment and focus on the controller:

angular.module('ovSampleCustom', [])
    .controller('OvSampleCustomCtrl', 
    [ '$log', '$scope', 'WebSocketService', 'KeyService',
 
    function (_$log_, _$scope_, _wss_, _ks_) { 
        ...
    }]);

The first line here gets a reference to the "ovSampleCustom" module. Again, this is 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 – "OvSampleCustomCtrl" – 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 (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 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:

$log = _$log_;
$scope = _$scope_;
wss = _wss_;
ks = _ks_;

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

 

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

// data response handler
handlers[dataResp] = respDataCb;
wss.bindHandlers(handlers);

The callback function (one of the helper methods we skipped earlier) has the job of setting our scope variable data to be the payload of the event, and then prodding angular to re-process:

function respDataCb(data) {
    $scope.data = data;
    $scope.$apply();
}

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

 

Next up, we add our keybindings:

addKeyBindings();

.. delegating to another of the previously skipped functions:

function addKeyBindings() {
    var map = {
        space: [getData, 'Fetch data from server'],
 
        _helpFormat: [
            ['space']
        ]
    };
 
    ks.keyBindings(map);
}

The map entries use well-known logical keys to identify the keystroke (in this example 'space' for the space-bar), with a value being a two-element array defining the callback function and the text to display in the Quick Help panel.

The _helpFormat key is a special value that is used by the Quick Help Service to lay out the definition of bound keystrokes; each sub-array in the main array is a "column" in the panel; the entries in each sub-array define the order in which the keys are listed.

See the Key Service for more information about binding keystrokes to actions, and for further information on the _helpFormat array.

The getData() function tells the WebSocketService to send the data request event to the server:

function getData() {
    wss.sendEvent(dataReq);
}

 

Remember the ng-click attribute in the "button" div in our HTML snippet? Well, here we set the function reference on our scope:

// custom click handler
$scope.getData = getData;

 

To populate our view the first time it is loaded, we need to send an initial event to the server:

// get data the first time...
getData();

 

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

// cleanup
$scope.$on('$destroy', function () {
    wss.unbindHandlers(handlers);
    ks.unbindKeys();
    $log.log('OvSampleCustomCtrl has been destroyed');
});

 

The last thing the controller does when it is initialized is to log to the console:

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

sampleCustom.css

Glue Files

css.html

js.html

 

  • No labels