Adding skeletal structure for the ONLP gNMI GUI demo.

Change-Id: I6796ebf200e20a51bdc098fcc3696b78d7c1132e
diff --git a/apps/onlp-demo/BUILD b/apps/onlp-demo/BUILD
new file mode 100644
index 0000000..0bf3560
--- /dev/null
+++ b/apps/onlp-demo/BUILD
@@ -0,0 +1,25 @@
+COMPILE_DEPS = CORE_DEPS + JACKSON + [
+    "@com_google_protobuf//:protobuf_java",
+    "@io_grpc_grpc_java//core",
+    "@io_grpc_grpc_java//netty",
+    "@io_grpc_grpc_java//stub",
+    "//core/store/serializers:onos-core-serializers",
+    "//protocols/gnmi/stub:onos-protocols-gnmi-stub",
+    "//protocols/gnmi/api:onos-protocols-gnmi-api",
+    "//protocols/grpc/api:onos-protocols-grpc-api",
+    "//protocols/grpc/proto:onos-protocols-grpc-proto",
+]
+
+osgi_jar_with_tests(
+    deps = COMPILE_DEPS,
+)
+
+onos_app(
+    category = "GUI",
+    description = "Provides a GUI overlay for displaying ONLP device management information.",
+    required_apps = [
+        "org.onosproject.protocols.gnmi",
+    ],
+    title = "ONLP device demo",
+    url = "http://onosproject.org",
+)
diff --git a/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/OnlpDemoManager.java b/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/OnlpDemoManager.java
new file mode 100644
index 0000000..6085bbc
--- /dev/null
+++ b/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/OnlpDemoManager.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.onlpdemo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import gnmi.Gnmi;
+import org.onlab.util.SharedExecutors;
+import org.onosproject.gnmi.api.GnmiClient;
+import org.onosproject.gnmi.api.GnmiController;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.config.NetworkConfigService;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiTopoOverlay;
+import org.onosproject.ui.UiTopoOverlayFactory;
+import org.onosproject.ui.UiView;
+import org.onosproject.ui.UiViewHidden;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.TimerTask;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Extends the ONOS GUI to display various ONLP device data.
+ */
+@Component(immediate = true, service = OnlpDemoManager.class)
+public class OnlpDemoManager {
+
+    private Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final String EXTENSION_ID = "onlpdemo";
+    private static final String OVERLAY_ID = "od-overlay";
+
+    private static final String OVERLAY_VIEW_ID = "odTopov";
+    private static final String TABLE_VIEW_ID = "onlp";
+
+    // List of application views
+    private final List<UiView> uiViews = ImmutableList.of(
+            new UiViewHidden(OVERLAY_VIEW_ID),
+            new UiViewHidden(TABLE_VIEW_ID)
+    );
+
+    // Factory for UI message handlers
+    private final UiMessageHandlerFactory messageHandlerFactory =
+            () -> ImmutableList.of(new OnlpDemoViewMessageHandler(new GnmiOnlpDataSource()));
+
+    // Factory for UI topology overlays
+    private final UiTopoOverlayFactory topoOverlayFactory =
+            () -> ImmutableList.of(new UiTopoOverlay(OVERLAY_ID));
+
+    // Application UI extension
+    protected UiExtension extension =
+            new UiExtension.Builder(getClass().getClassLoader(), uiViews)
+                    .resourcePath(EXTENSION_ID)
+                    .messageHandlerFactory(messageHandlerFactory)
+                    .topoOverlayFactory(topoOverlayFactory)
+                    .build();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected NetworkConfigService networkConfigService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected UiExtensionService uiExtensionService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected GnmiController gnmiController;
+
+    @Activate
+    protected void activate() {
+        uiExtensionService.register(extension);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        uiExtensionService.unregister(extension);
+        log.info("Stopped");
+    }
+
+
+    public class OnlpData {
+        String id;
+        String presence;
+        String vendor;
+        String modelNumber;
+        String serialNumber;
+        String formFactor;
+
+        OnlpData(String id, String presence, String vendor, String modelNumber,
+                 String serialNumber, String formFactor) {
+            this.id = id;
+            this.presence = presence;
+            this.vendor = vendor;
+            this.modelNumber = modelNumber;
+            this.serialNumber = serialNumber;
+            this.formFactor = formFactor;
+        }
+    }
+
+    public interface OnlpDataSource {
+        List<OnlpData> getData(DeviceId deviceId);
+    }
+
+
+    public class GnmiOnlpDataSource implements OnlpDataSource {
+
+        private Map<DeviceId, List<OnlpData>> cache = Maps.newConcurrentMap();
+
+        GnmiOnlpDataSource() {
+            SharedExecutors.getTimer().schedule(new TimerTask() {
+                @Override
+                public void run() {
+                    cache.keySet().forEach(GnmiOnlpDataSource.this::fetchData);
+                }
+            }, 5000, 5000);
+        }
+
+        @Override
+        public List<OnlpData> getData(DeviceId deviceId) {
+            return cache.computeIfAbsent(deviceId, k -> ImmutableList.of());
+        }
+
+        private void fetchData(DeviceId deviceId) {
+            ImmutableList.Builder<OnlpData> builder = ImmutableList.builder();
+            GnmiClient gnmiClient = gnmiController.getClient(deviceId);
+            deviceService.getPorts(deviceId)
+                    .forEach(port -> builder.add(getOnlpData(gnmiClient, port)));
+            cache.put(deviceId, builder.build());
+        }
+
+        private OnlpData getOnlpData(GnmiClient gnmiClient, Port port) {
+            CompletableFuture<Gnmi.GetResponse> prReq = gnmiClient.get(fieldRequest(port, "present"));
+            CompletableFuture<Gnmi.GetResponse> veReq = gnmiClient.get(fieldRequest(port, "vendor"));
+            CompletableFuture<Gnmi.GetResponse> snReq = gnmiClient.get(fieldRequest(port, "serial-no"));
+            CompletableFuture<Gnmi.GetResponse> vpReq = gnmiClient.get(fieldRequest(port, "vendor-part"));
+            CompletableFuture<Gnmi.GetResponse> ffReq = gnmiClient.get(fieldRequest(port, "form-factor"));
+
+            return new OnlpData("sfp-" + port.number().name().replaceFirst("/[0-9]", ""),
+                                value(prReq).equals("PRESENT") ? "*" : "",
+                                value(veReq), value(vpReq), value(snReq), value(ffReq));
+        }
+
+        private String value(CompletableFuture<Gnmi.GetResponse> req) {
+            Gnmi.GetResponse response = Futures.getUnchecked(req);
+            return response.getNotificationList().isEmpty() ?
+                    "" : response.getNotification(0).getUpdate(0).getVal().getStringVal().trim();
+        }
+
+        private Gnmi.GetRequest fieldRequest(Port port, String field) {
+            Gnmi.Path path = Gnmi.Path.newBuilder()
+                    .addElem(Gnmi.PathElem.newBuilder().setName("components").build())
+                    .addElem(Gnmi.PathElem.newBuilder().setName("component").putKey("name",
+                                                                                    port.number().name()).build())
+                    .addElem(Gnmi.PathElem.newBuilder().setName("transceiver").build())
+                    .addElem(Gnmi.PathElem.newBuilder().setName("state").build())
+                    .addElem(Gnmi.PathElem.newBuilder().setName(field).build())
+                    .build();
+            return Gnmi.GetRequest.newBuilder()
+                    .addPath(path)
+                    .setType(Gnmi.GetRequest.DataType.ALL)
+                    .setEncoding(Gnmi.Encoding.PROTO)
+                    .build();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/OnlpDemoViewMessageHandler.java b/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/OnlpDemoViewMessageHandler.java
new file mode 100644
index 0000000..3a37481
--- /dev/null
+++ b/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/OnlpDemoViewMessageHandler.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.onlpdemo;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.net.DeviceId;
+import org.onosproject.onlpdemo.OnlpDemoManager.OnlpData;
+import org.onosproject.onlpdemo.OnlpDemoManager.OnlpDataSource;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.table.TableModel;
+import org.onosproject.ui.table.TableRequestHandler;
+import org.onosproject.ui.table.cell.AbstractCellComparator;
+
+import java.util.Collection;
+
+
+/**
+ * Message handler for ONLP view related messages.
+ */
+public class OnlpDemoViewMessageHandler extends UiMessageHandler {
+
+    private static final String ONLP_DATA_REQ = "onlpDataRequest";
+    private static final String ONLP_DATA_RESP = "onlpDataResponse";
+    private static final String ONLPS = "onlps";
+
+    private static final String ID = "id";
+    private static final String PRESENCE = "presence";
+    private static final String VENDOR = "vendor";
+    private static final String SERIAL_NO = "serial_number";
+    private static final String MODEL_NO = "model_number";
+    private static final String FORM_FACTOR = "form_factor";
+
+
+    private static final String[] COL_IDS = {
+            ID, PRESENCE, VENDOR, MODEL_NO, SERIAL_NO, FORM_FACTOR,
+    };
+
+    private final OnlpDataSource onlpDataSource;
+
+    public OnlpDemoViewMessageHandler(OnlpDataSource onlpDataSource) {
+        super();
+        this.onlpDataSource = onlpDataSource;
+    }
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(
+                new OnlpDataRequest()
+        );
+    }
+
+    // handler for port table requests
+    private final class OnlpDataRequest extends TableRequestHandler {
+
+        private static final String NO_ROWS_MESSAGE = "No data available yet";
+
+        private OnlpDataRequest() {
+            super(ONLP_DATA_REQ, ONLP_DATA_RESP, ONLPS);
+        }
+
+        @Override
+        protected String[] getColumnIds() {
+            return COL_IDS;
+        }
+
+        @Override
+        protected String noRowsMessage(ObjectNode payload) {
+            return NO_ROWS_MESSAGE;
+        }
+
+        @Override
+        protected TableModel createTableModel() {
+            TableModel tm = super.createTableModel();
+            tm.setComparator(ID, new SfpIdCellComparator());
+            return tm;
+        }
+
+        @Override
+        protected void populateTable(TableModel tm, ObjectNode payload) {
+            String uri = string(payload, "devId");
+            if (!Strings.isNullOrEmpty(uri)) {
+                for (OnlpData data : onlpDataSource.getData(DeviceId.deviceId(uri))) {
+                    populateRow(tm.addRow(), data);
+                }
+            }
+        }
+
+        private void populateRow(TableModel.Row row, OnlpData data) {
+            row.cell(ID, data.id)
+                .cell(PRESENCE, data.presence)
+                .cell(VENDOR, data.vendor)
+                .cell(MODEL_NO, data.modelNumber)
+                .cell(SERIAL_NO, data.serialNumber)
+                .cell(FORM_FACTOR, data.formFactor);
+        }
+    }
+
+    private static class SfpIdCellComparator extends AbstractCellComparator {
+        @Override
+        protected int nonNullCompare(Object o1, Object o2) {
+            return index((String) o2) - index((String) o1);
+        }
+
+        private int index(String s) {
+            int i = s.indexOf("-");
+            try {
+                return i > 0 ? Integer.parseInt(s.substring(i)) : 0;
+            } catch (NumberFormatException e) {
+                return Integer.MAX_VALUE;
+            }
+        }
+    }
+
+}
diff --git a/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/package-info.java b/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/package-info.java
new file mode 100644
index 0000000..c049790
--- /dev/null
+++ b/apps/onlp-demo/src/main/java/org/onosproject/onlpdemo/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 20199-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Demo GUI for displaying ONLP device data obtained via gNMI.
+ */
+package org.onosproject.onlpdemo;
\ No newline at end of file
diff --git a/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.css b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.css
new file mode 100644
index 0000000..8aa18f6
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.css
@@ -0,0 +1,2 @@
+/* css for layout topology overlay  */
+
diff --git a/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.html b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.html
new file mode 100644
index 0000000..377b3b0
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.html
@@ -0,0 +1,4 @@
+<!-- partial HTML -->
+<div id="ov-od-topov">
+    <p>This is a hidden view .. just a placeholder to house the javascript</p>
+</div>
diff --git a/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.js b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.js
new file mode 100644
index 0000000..d78c009
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopov.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ Module containing the "business logic" for the layout topology overlay.
+ */
+
+(function () {
+    'use strict';
+
+    // injected refs
+    var $log, flash, wss;
+
+    function doFoo(type, description) {
+        flash.flash(description);
+        wss.sendEvent('doFoo', {
+            type: type
+        });
+    }
+
+    function clear() {
+        // Nothing to do?
+    }
+
+    angular.module('ovOdTopov', [])
+        .factory('OnlpDemoTopovService',
+        ['$log', 'FlashService', 'WebSocketService',
+
+        function (_$log_, _flash_, _wss_) {
+            $log = _$log_;
+            flash = _flash_;
+            wss = _wss_;
+
+            return {
+                doFoo: doFoo,
+                clear: clear
+            };
+        }]);
+}());
diff --git a/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopovOverlay.js b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopovOverlay.js
new file mode 100644
index 0000000..682ea17
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/app/view/odTopov/odTopovOverlay.js
@@ -0,0 +1,59 @@
+// path painter topology overlay - client side
+//
+// This is the glue that binds our business logic (in ppTopov.js)
+// to the overlay framework.
+
+(function () {
+    'use strict';
+
+    // injected refs
+    var $log, tov, ns, lts, sel;
+
+    // our overlay definition
+    var overlay = {
+        overlayId: 'od-overlay',
+        glyphId: 'm_disjointPaths',
+        tooltip: 'ONLP Data Overlay',
+
+        activate: function () {
+            $log.debug("ONLP data topology overlay ACTIVATED");
+        },
+        deactivate: function () {
+            lts.clear();
+            $log.debug("ONLP data topology overlay DEACTIVATED");
+        },
+
+        // detail panel button definitions
+        buttons: {
+            showOnlpView: {
+                gid: 'chain',
+                tt: 'ONLP data',
+                cb: function (data) {
+                    $log.debug('ONLP action invoked on selection:', sel);
+                    ns.navTo('onlp', { devId: sel.id });
+                }
+            }
+        },
+
+        hooks: {
+            // hooks for when the selection changes...
+            single: function (data) {
+                $log.debug('selection data:', data);
+                sel = data;
+                tov.addDetailButton('showOnlpView');
+            }
+        }
+    };
+
+    // invoke code to register with the overlay service
+    angular.module('ovOdTopov')
+        .run(['$log', 'TopoOverlayService', 'NavService', 'OnlpDemoTopovService',
+
+            function (_$log_, _tov_, _ns_, _lts_) {
+                $log = _$log_;
+                tov = _tov_;
+                ns = _ns_;
+                lts = _lts_;
+                tov.register(overlay);
+            }]);
+}());
diff --git a/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.css b/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.css
new file mode 100644
index 0000000..60e1c58
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.css
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Port View (layout) -- CSS file
+ */
+
+#ov-onlp h2 {
+    display: inline-block;
+}
+
+#ov-onlp div.ctrl-btns {
+}
+
+#ov-onlp td {
+    text-align: center;
+}
+
+#ov-onlp td.left {
+    text-align: left;
+}
+
+#ov-onlp tr.left {
+    text-align: left;
+}
+
+#ov-onlp tr.no-data td {
+    text-align: center;
+}
diff --git a/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.html b/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.html
new file mode 100644
index 0000000..6972f40
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.html
@@ -0,0 +1,91 @@
+<!-- Port partial HTML -->
+<div id="ov-onlp">
+    <div class="tabular-header">
+        <h2>ONLP Data for {{devId || "(No device selected)"}}</h2>
+
+        <div class="ctrl-btns">
+            <div class="refresh" ng-class="{active: autoRefresh}"
+                 icon icon-size="42" icon-id="refresh"
+                 tooltip tt-msg="autoRefreshTip"
+                 ng-click="toggleRefresh()"></div>
+
+            <div class="separator"></div>
+
+            <div class="refresh" ng-class="{active: isNZ()}"
+                 icon icon-size="42" icon-id="nonzero"
+                 tooltip tt-msg="toggleNZTip"
+                 ng-click="toggleNZ()"></div>
+
+            <div class="refresh" ng-class="{active: isDelta()}"
+                 icon icon-size="42" icon-id="delta"
+                 tooltip tt-msg="toggleDeltaTip"
+                 ng-click="toggleDelta()"></div>
+
+            <div class="separator"></div>
+
+            <div class="active"
+                 icon icon-id="deviceTable" icon-size="42"
+                 tooltip tt-msg="deviceTip"
+                 ng-click="nav('device')"></div>
+
+            <div class="active"
+                 icon icon-id="flowTable" icon-size="42"
+                 tooltip tt-msg="flowTip"
+                 ng-click="nav('flow')"></div>
+
+            <div class="current-view"
+                 icon icon-id="portTable" icon-size="42"></div>
+
+            <div class="active"
+                 icon icon-id="groupTable" icon-size="42"
+                 tooltip tt-msg="groupTip"
+                 ng-click="nav('group')"></div>
+
+            <div class="active"
+                 icon icon-id="meterTable" icon-size="42"
+                 tooltip tt-msg="meterTip"
+                 ng-click="nav('meter')"></div>
+
+            <div class="active"
+                 icon icon-id="pipeconfTable" icon-size="42"
+                 tooltip tt-msg="pipeconfTip"
+                 ng-click="nav('pipeconf')"></div>
+        </div>
+    </div>
+
+
+     <div class="summary-list" onos-table-resize>
+        <div class="table-header" onos-sortable-header>
+            <table>
+                <tr>
+                    <td colId="id" col-width="60px" sortable>SFP ID </td>
+                    <td class="left" colId="id" col-width="30px" sortable> </td>
+                    <td class="left" colId="vendor" sortable>Vendor </td>
+                    <td class="left" colId="model_number" sortable>Model # </td>
+                    <td class="left" colId="serial_number" sortable>Serial # </td>
+                    <td class="left" colId="form_factor" sortable>Form Factor </td>
+                </tr>
+            </table>
+        </div>
+
+        <div class="table-body">
+            <table id-prop="id">
+                <tr ng-if="!tableData.length" class="no-data">
+                    <td colspan="6">{{annots.no_rows_msg}}</td>
+                </tr>
+
+                <tr ng-repeat="onlp in tableData"
+                    ng-class="{selected: onlp.id === selId}"
+                    ng-repeat-complete row-id="{{onlp.id}}">
+                    <td>{{onlp.id}}</td>
+                    <td class="left">{{onlp.presence}}</td>
+                    <td class="left">{{onlp.vendor}}</td>
+                    <td class="left">{{onlp.model_number}}</td>
+                    <td class="left">{{onlp.serial_number}}</td>
+                    <td class="left">{{onlp.form_factor}}</td>
+                </tr>
+            </table>
+        </div>
+    </div>
+
+</div>
diff --git a/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.js b/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.js
new file mode 100644
index 0000000..81625d2
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/app/view/onlp/onlp.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Onlp View Module
+ */
+
+(function () {
+    'use strict';
+
+    // injected references
+    var $log, $scope, $location, tbs, fs, mast, wss, ns, prefs, is, ps;
+
+    // internal state
+    var nzFilter = true,
+        showDelta = false,
+        pStartY,
+        pHeight,
+        wSize;
+
+    var keyBindings = {
+    };
+
+    var defaultOnlpPrefsState = {
+    };
+
+    var prefsState = {};
+
+    function updatePrefsState(what, b) {
+        prefsState[what] = b ? 1 : 0;
+        prefs.setPrefs('onlp_prefs', prefsState);
+    }
+
+    function restoreConfigFromPrefs() {
+        prefsState = prefs.asNumbers(
+            prefs.getPrefs('onlp_prefs', defaultOnlpPrefsState)
+        );
+
+        $log.debug('ONLP - Prefs State:', prefsState);
+    }
+
+    angular.module('ovOnlp', [])
+        .controller('OvOnlpCtrl', [
+            '$log', '$scope', '$location',
+            'TableBuilderService', 'FnService', 'MastService', 'WebSocketService',
+            'NavService', 'PrefsService', 'IconService',
+            'PanelService',
+
+            function (_$log_, _$scope_, _$location_,
+                      _tbs_, _fs_, _mast_, _wss_,
+                      _ns_, _prefs_, _is_, _ps_) {
+                var params;
+                var tableApi;
+                $log = _$log_;
+                $scope = _$scope_;
+                $location = _$location_;
+                tbs = _tbs_;
+                fs = _fs_;
+                mast = _mast_;
+                wss = _wss_;
+                ns = _ns_;
+                prefs = _prefs_;
+                is = _is_;
+                ps = _ps_;
+
+                params = $location.search();
+
+                $scope.deviceTip = 'Show device table';
+                $scope.flowTip = 'Show flow view for this device';
+                $scope.groupTip = 'Show group view for this device';
+                $scope.meterTip = 'Show meter view for selected device';
+                $scope.pipeconfTip = 'Show pipeconf view for selected device';
+
+                if (params.hasOwnProperty('devId')) {
+                    $scope.devId = params['devId'];
+                }
+
+                $scope.payloadParams = {
+                    nzFilter: nzFilter,
+                    showDelta: showDelta,
+                };
+
+                tableApi = tbs.buildTable({
+                    scope: $scope,
+                    tag: 'onlp',
+                    query: params,
+                });
+
+                function filterToggleState() {
+                    return {
+                        nzFilter: nzFilter,
+                        showDelta: showDelta,
+                    };
+                }
+
+                $scope.nav = function (path) {
+                    if ($scope.devId) {
+                        ns.navTo(path, { devId: $scope.devId });
+                    }
+                };
+
+                function getOperatorFromQuery(query) {
+
+                    var operator = query.split(' '),
+                        opFunc = null;
+
+                    if (operator[0] === '>') {
+                        opFunc = _.gt;
+                    } else if (operator[0] === '>=') {
+                        opFunc = _.gte;
+                    } else if (operator[0] === '<') {
+                        opFunc = _.lt;
+                    } else if (operator[0] === '<=') {
+                        opFunc = _.lte;
+                    } else {
+                        return {
+                            operator: opFunc,
+                            searchText: query,
+                        };
+                    }
+
+                    return {
+                        operator: opFunc,
+                        searchText: operator[1],
+                    };
+                }
+
+                $scope.customFilter = function (prop, val) {
+                    if (!val) {
+                        return;
+                    }
+
+                    var search = getOperatorFromQuery(val),
+                        operator = search.operator,
+                        searchText = search.searchText;
+
+                    if (operator) {
+                        return function (row) {
+                            var queryBy = $scope.queryBy || '$';
+
+                            if (queryBy !== '$') {
+                                var rowValue = parseInt(row[$scope.queryBy].replace(/,/g, ''));
+                                return operator(rowValue, parseInt(searchText)) ? row : null;
+                            } else {
+                                var keys = _.keysIn(row);
+
+                                for (var i = 0, l = keys.length; i < l; i++) {
+                                    var rowValue = parseInt(row[keys[i]].replace(/,/g, ''));
+                                    if (operator(rowValue, parseInt(searchText))) {
+                                        return row;
+                                    }
+                                }
+                            }
+                        };
+                    } else {
+                        var out = {};
+                        out[$scope.queryBy || '$'] = $scope.query;
+                        return out;
+                    }
+                };
+
+                restoreConfigFromPrefs();
+
+                $scope.$on('$destroy', function () {
+                    dps.destroy();
+                });
+
+                $log.log('OvOnlpCtrl has been created');
+            }]);
+}());
diff --git a/apps/onlp-demo/src/main/resources/onlpdemo/css.html b/apps/onlp-demo/src/main/resources/onlpdemo/css.html
new file mode 100644
index 0000000..ace7ac3
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/onlpdemo/css.html
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright 2019-present Open Networking Foundation
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<link rel="stylesheet" href="app/view/odTopov/odTopov.css">
+<link rel="stylesheet" href="app/view/onlp/onlp.css">
\ No newline at end of file
diff --git a/apps/onlp-demo/src/main/resources/onlpdemo/js.html b/apps/onlp-demo/src/main/resources/onlpdemo/js.html
new file mode 100644
index 0000000..6a45bad
--- /dev/null
+++ b/apps/onlp-demo/src/main/resources/onlpdemo/js.html
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright 2019-present Open Networking Foundation
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<script src="app/view/onlp/onlp.js"></script>
+<script src="app/view/odTopov/odTopov.js"></script>
+<script src="app/view/odTopov/odTopovOverlay.js"></script>
diff --git a/tools/build/bazel/modules.bzl b/tools/build/bazel/modules.bzl
index 6a1603f..0fb877e 100644
--- a/tools/build/bazel/modules.bzl
+++ b/tools/build/bazel/modules.bzl
@@ -244,6 +244,7 @@
     "//apps/odtn/service:onos-apps-odtn-service-oar",
     "//apps/mcast:onos-apps-mcast-oar",
     "//apps/layout:onos-apps-layout-oar",
+    "//apps/onlp-demo:onos-apps-onlp-demo-oar",
     "//apps/imr:onos-apps-imr-oar",
     "//apps/nodemetrics:onos-apps-nodemetrics-oar",
     "//apps/inbandtelemetry:onos-apps-inbandtelemetry-oar",