UI-Ref : added example use of chained dialogs in topology overlay, illustrating how to get additional data from the server, and to allow user to make a sequence of selections.

Change-Id: Ice882b45fc9e378e134023fc40b81747a954d19f
diff --git a/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlay.java b/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlay.java
index 63b65ba..3553824 100644
--- a/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlay.java
+++ b/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlay.java
@@ -41,8 +41,8 @@
     private static final String MY_VERSION = "Beta-1.0.0042";
     private static final String MY_DEVICE_TITLE = "I changed the title";
 
-    private static final ButtonId FOO_BUTTON = new ButtonId("foo");
-    private static final ButtonId BAR_BUTTON = new ButtonId("bar");
+    private static final ButtonId SIMPLE_D_BUTTON = new ButtonId("simpleDialog");
+    private static final ButtonId CHAIN_D_BUTTON = new ButtonId("chainDialog");
 
     public UiRefTopoOverlay() {
         super(OVERLAY_ID);
@@ -51,6 +51,11 @@
 
     @Override
     public void modifySummary(PropertyPanel pp) {
+        // NOTE: if you don't want any of the original content you can
+        //       use the following convenience methods:
+//        pp.removeAllButtons();
+//        pp.removeAllProps();
+
         pp.title(MY_TITLE)
                 .typeId(TopoConstants.Glyphs.CROWN)
                 .removeProps(
@@ -68,8 +73,8 @@
         pp.title(MY_DEVICE_TITLE);
         pp.removeProps(LATITUDE, LONGITUDE);
 
-        pp.addButton(FOO_BUTTON)
-                .addButton(BAR_BUTTON);
+        pp.addButton(SIMPLE_D_BUTTON)
+                .addButton(CHAIN_D_BUTTON);
 
         pp.removeButtons(TopoConstants.CoreButtons.SHOW_PORT_VIEW)
                 .removeButtons(TopoConstants.CoreButtons.SHOW_GROUP_VIEW);
diff --git a/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlayMessageHandler.java b/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlayMessageHandler.java
index 18fc4e2..2fe5669 100644
--- a/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlayMessageHandler.java
+++ b/uiref/src/main/java/org/onosproject/uiref/UiRefTopoOverlayMessageHandler.java
@@ -16,6 +16,7 @@
 
 package org.onosproject.uiref;
 
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
@@ -25,9 +26,11 @@
 import org.onosproject.net.Element;
 import org.onosproject.net.HostId;
 import org.onosproject.net.Link;
+import org.onosproject.net.Port;
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.host.HostService;
 import org.onosproject.net.link.LinkService;
+import org.onosproject.ui.JsonUtils;
 import org.onosproject.ui.RequestHandler;
 import org.onosproject.ui.UiConnection;
 import org.onosproject.ui.UiMessageHandler;
@@ -40,6 +43,7 @@
 
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.Timer;
 import java.util.TimerTask;
@@ -52,10 +56,22 @@
     private static final String UI_REF_TOPOV_DISPLAY_START = "uiRefTopovDisplayStart";
     private static final String UI_REF_TOPOV_DISPLAY_UPDATE = "uiRefTopovDisplayUpdate";
     private static final String UI_REF_TOPOV_DISPLAY_STOP = "uiRefTopovDisplayStop";
+    private static final String UI_REF_TOPOV_DEV_PORTS_REQ = "uiRefTopovDevicePortsReq";
+    private static final String UI_REF_TOPOV_DEV_PORTS_RESP = "uiRefTopovDevicePortsResp";
+    private static final String UI_REF_TOPOV_DEV_PORTS_OP = "uiRefTopovDevicePortFakeOp";
 
     private static final String ID = "id";
+
+    private static final String PORTS = "ports";
+    private static final String SPEED = "speed";
+    private static final String TYPE = "type";
     private static final String MODE = "mode";
 
+    private static final String DEVICE = "device";
+    private static final String PORT = "port";
+    private static final String FOO = "foo";
+    private static final String BAR = "bar";
+
     private static final long UPDATE_PERIOD_MS = 1000;
 
     private static final Link[] EMPTY_LINK_SET = new Link[0];
@@ -92,7 +108,9 @@
         return ImmutableSet.of(
                 new DisplayStartHandler(),
                 new DisplayUpdateHandler(),
-                new DisplayStopHandler()
+                new DisplayStopHandler(),
+                new DevicePortHandler(),
+                new PortFakeOpHandler()
         );
     }
 
@@ -100,7 +118,7 @@
     // === Handler classes
 
     private final class DisplayStartHandler extends RequestHandler {
-        public DisplayStartHandler() {
+        DisplayStartHandler() {
             super(UI_REF_TOPOV_DISPLAY_START);
         }
 
@@ -135,7 +153,7 @@
     }
 
     private final class DisplayUpdateHandler extends RequestHandler {
-        public DisplayUpdateHandler() {
+        DisplayUpdateHandler() {
             super(UI_REF_TOPOV_DISPLAY_UPDATE);
         }
 
@@ -152,7 +170,7 @@
     }
 
     private final class DisplayStopHandler extends RequestHandler {
-        public DisplayStopHandler() {
+        DisplayStopHandler() {
             super(UI_REF_TOPOV_DISPLAY_STOP);
         }
 
@@ -165,8 +183,79 @@
         }
     }
 
+    private final class DevicePortHandler extends RequestHandler {
+        DevicePortHandler() {
+            super(UI_REF_TOPOV_DEV_PORTS_REQ);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            String id = string(payload, ID);
+            log.debug("Request ports for device [{}]", id);
+            sendPortDataForDevice(id);
+        }
+    }
+
+    private final class PortFakeOpHandler extends RequestHandler {
+        PortFakeOpHandler() {
+            super(UI_REF_TOPOV_DEV_PORTS_OP);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            String device = string(payload, DEVICE);
+            String port = string(payload, PORT);
+
+// TODO: re-instate boolean results once onos-app-samples -> onos dep. fixed
+//            boolean foo = bool(payload, FOO);
+//            boolean bar = bool(payload, BAR);
+
+            // NOTE: we won't go any further with this example, but obviously
+            //       we could initiate some action on the device and port
+            //       selected by the user from the Topology view in the Web UI.
+            log.info("FAKE-op request device {} port {}", device, port);
+//            log.info("    options FOO={}, BAR={}", foo, bar);
+        }
+    }
+
     // === ------------
 
+    private void sendPortDataForDevice(String id) {
+        try {
+            DeviceId did = DeviceId.deviceId(id);
+            List<Port> ports = deviceService.getPorts(did);
+            log.debug("Sending port data for device {} (#ports: {})", did, ports.size());
+            ArrayNode portArray = arrayNode();
+            for (Port p : ports) {
+                // don't bother to send logical ports
+                if (p.number().isLogical()) {
+                    continue;
+                }
+                // NOTE: we could just add the port numbers (as longs) to the
+                //       array and have done, but let's demonstrate how we
+                //       might build more complex record types...
+                portArray.add(portData(p));
+            }
+
+            ObjectNode payload = objectNode();
+            payload.set(PORTS, portArray);
+            payload.put(ID, id);
+            sendMessage(JsonUtils.envelope(UI_REF_TOPOV_DEV_PORTS_RESP, payload));
+
+        } catch (Exception e) {
+            log.warn("[port data] Unable to process ID [{}]", id);
+        }
+    }
+
+    private ObjectNode portData(Port p) {
+        ObjectNode node = objectNode();
+        node.put(ID, p.number().toLong());
+        node.put(SPEED, p.portSpeed());
+        node.put(TYPE, p.type().name());
+        return node;
+    }
+
+
     private void clearState() {
         currentMode = Mode.IDLE;
         elementOfNote = null;
@@ -191,7 +280,7 @@
                 log.debug("device element {}", elementOfNote);
 
             } catch (Exception e2) {
-                log.debug("Unable to process ID [{}]", id);
+                log.warn("Unable to process ID [{}]", id);
                 elementOfNote = null;
             }
         }
diff --git a/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovDemo.js b/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovDemo.js
index 803ec59..839633f 100644
--- a/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovDemo.js
+++ b/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovDemo.js
@@ -10,14 +10,136 @@
     var $log, fs, flash, wss, tss, tds;
 
     // constants
-    var displayStart = 'uiRefTopovDisplayStart',
-        displayUpdate = 'uiRefTopovDisplayUpdate',
-        displayStop = 'uiRefTopovDisplayStop';
+    var pfx = 'uiRefTopov',
+        displayStart = pfx + 'DisplayStart',
+        displayUpdate = pfx + 'DisplayUpdate',
+        displayStop = pfx + 'DisplayStop',
+        portsReq = pfx + 'DevicePortsReq',
+        portsResp = pfx + 'DevicePortsResp',
+        portsOp = pfx + 'DevicePortFakeOp';
 
     // internal state
-    var currentMode = null;
+    var currentMode = null,
+        ctx = {                 // chained dialog context
+            device: null,
+            port: -1,
+            foo: false,
+            bar: false
+        },
+        handlers = {};
 
 
+    // Handle device ports response from server:
+    //   This will be invoked in response to a device selected and the
+    //   "chain" button pressed on the details dialog, once the response
+    //   comes back from the server.
+    // We are going to open a dialog and ask the user to select one
+    //   of the ports for the device...
+    handlers[portsResp] = function (data) {
+        $log.debug('hey! Port Data from the server!', data);
+        ctx.device = data.id;
+        
+        // invoked when the OK button is pressed on this dialog
+        function dOk() {
+            $log.debug('Dialog OK button pressed');
+            portOptionsDialog();
+        }
+
+        tds.openDialog()
+            .setTitle('Choose Port')
+            .addContent(createPortChoiceContent(data.ports))
+            .addCancel()
+            .addOkChained(dOk)      // NOTE: we use the "chained" version here
+            .bindKeys();
+    };
+
+    function createPortChoiceContent(ports) {
+        var content = tds.createDiv('port-list'),
+            form,
+            portSelect;
+
+        content.append('p').text('Select port of device ' + ctx.device);
+        form = content.append('form');
+        form.append('span').text('port number: ');
+
+        // onchange function for selection widget
+        function selectPort() {
+            ctx.port = this.options[this.selectedIndex].value;
+        }
+
+        portSelect = form.append('select').on('change', selectPort);
+        ports.forEach(function (p) {
+            portSelect.append('option')
+                .attr('value', p.id)
+                .text(p.id);
+        });
+        
+        ctx.port = -1;      // clear state from any previous invocations
+
+        return content;
+    }
+
+
+    // the function that is called if OK is pressed on our ports dialog
+    function portOptionsDialog() {
+
+        // invoked when the OK button is pressed on this dialog
+        function dOk() {
+            $log.debug('Port Options Dialog OK button pressed');
+            $log.debug('Sending event', portsOp, ctx);
+            wss.sendEvent(portsOp, ctx);
+        }
+
+        tds.openDialog()
+            .setTitle('Select Port Options')
+            .addContent(createPortOptionsContent())
+            .addCancel()
+            .addOk(dOk)     // NOTE: NOT the "chained" version!
+            .bindKeys();
+    }
+
+    function createPortOptionsContent() {
+        var content = tds.createDiv('port-opts'),
+            form;
+
+        // helper function to add a paragraph
+        function para(text) {
+            content.append('p').text(text);
+        }
+
+        para('Device ' + ctx.device);
+        para('Port ' + ctx.port);
+
+        form = content.append('form');
+
+        // helper function to add a checkbox to the form, which updates the
+        //  context when the user toggles the checked state of the box.
+        function cbox(name, val) {
+
+            // onchange function for checkbox widget
+            function onchange() {
+                ctx[val] = this.checked;
+            }
+
+            form.append('input').attr({
+                type: 'checkbox',
+                name: name,
+                value: val
+            }).on('change', onchange);
+
+            ctx[val] = false;   // clear state from any previous invocations
+
+            form.append('span').text(name);
+            form.append('br');
+        }
+
+        // add two checkboxes...
+        cbox('Foo', 'foo');
+        cbox('Bar', 'bar');
+
+        return content;
+    }
+
     // === ---------------------------
     // === Helper functions
 
@@ -40,6 +162,7 @@
     function createDialogContent(devs) {
         var content = tds.createDiv('my-content-class'),
             items;
+        
         content.append('p').text('Do something to these devices?');
         items = content.append('div');
         devs.forEach(function (d) {
@@ -48,25 +171,25 @@
         return content;
     }
 
-    function dCancel() {
-        $log.debug('Dialog CANCEL button pressed');
-    }
 
-    function dOk() {
-        $log.debug('Dialog OK button pressed');
-    }
-
-    function createListContent() {
-        var content = tds.createDiv('my-list-class'),
-            items;
-        // TODO: figure out best way to inject selectable list
-        content.append('p').text('(Selectable list to show here...)');
+    function createCustomContent() {
+        var content = tds.createDiv('my-div-class');
+        content.append('p').text('(Some content goes here...)');
         return content;
     }
 
     // === ---------------------------
     // === Main API functions
 
+    function overlayActive(active) {
+        if (active) {
+            wss.bindHandlers(handlers);
+        } else {
+            stopDisplay();
+            wss.unbindHandlers(handlers);
+        }
+    }
+
     function startDisplay(mode) {
         if (currentMode === mode) {
             $log.debug('(in mode', mode, 'already)');
@@ -93,31 +216,59 @@
         return false;
     }
 
-    // this example dialog invoked from the details panel, when one or more
-    //  devices have been selected
-    function deviceDialog() {
-        var ctx = tss.selectionContext();
+    // this example dialog is invoked from the details panel, when one or more
+    //  devices have been selected, and the "banner" button is pressed.
+    function simpleDialog() {
+        var sctx = tss.selectionContext();
 
-        $log.debug('device dialog invoked with context:', ctx);
+        $log.debug('SIMPLE: device dialog invoked with context:', sctx);
+
+        function dCancel() {
+            $log.debug('Dialog CANCEL button pressed');
+        }
+
+        function dOk() {
+            $log.debug('Dialog OK button pressed');
+        }
 
         // only if at least one device was selected
-        if (ctx.devices.length) {
+        if (sctx.devices.length) {
             tds.openDialog()
                 .setTitle('Process Devices')
-                .addContent(createDialogContent(ctx.devices))
+                .addContent(createDialogContent(sctx.devices))
                 .addCancel(dCancel)     // 'esc' key bound to 'Cancel' button
                 .addOk(dOk)             // 'enter' key bound to 'OK' button
                 .bindKeys();
         }
     }
 
+    // this example dialog is invoked from the details panel, when a single 
+    // device has been selected and the "chain" button is pressed.
+    function chainedDialogs() {
+        var sctx = tss.selectionContext();
+
+        $log.debug('CHAINED: device dialog invoked with context:', sctx);
+
+        // only if exactly one device was selected...
+        if (sctx.devices.length === 1) {
+            // send a request for port information about the device to server
+            wss.sendEvent(portsReq, {
+                id: sctx.devices[0]
+            });
+        }
+    }
+
     // this example dialog invoked from the toolbar
     function listDialog() {
         $log.debug('list dialog invoked');
 
+        function dOk() {
+            $log.debug('Dialog Gotcha button pressed');
+        }
+
         tds.openDialog()
             .setTitle('A list of stuff')
-            .addContent(createListContent())
+            .addContent(createCustomContent())
             .addOk(dOk, 'Gotcha')  // custom text for "OK" button
             .bindKeys();
     }
@@ -127,24 +278,27 @@
 
     angular.module('ovUiRefTopov', [])
         .factory('UiRefTopovDemoService',
-        ['$log', 'FnService', 'FlashService', 'WebSocketService',
-            'TopoSelectService', 'TopoDialogService',
+            ['$log', 'FnService', 'FlashService', 'WebSocketService',
+                'TopoSelectService', 'TopoDialogService',
 
-            function (_$log_, _fs_, _flash_, _wss_, _tss_, _tds_) {
-                $log = _$log_;
-                fs = _fs_;
-                flash = _flash_;
-                wss = _wss_;
-                tss = _tss_;
-                tds = _tds_;
+                function (_$log_, _fs_, _flash_, _wss_, _tss_, _tds_) {
+                    $log = _$log_;
+                    fs = _fs_;
+                    flash = _flash_;
+                    wss = _wss_;
+                    tss = _tss_;
+                    tds = _tds_;
 
-                return {
-                    startDisplay: startDisplay,
-                    updateDisplay: updateDisplay,
-                    stopDisplay: stopDisplay,
+                    return {
+                        overlayActive: overlayActive,
 
-                    deviceDialog: deviceDialog,
-                    listDialog: listDialog
-                };
-            }]);
+                        startDisplay: startDisplay,
+                        updateDisplay: updateDisplay,
+                        stopDisplay: stopDisplay,
+
+                        chainedDialogs: chainedDialogs,
+                        simpleDialog: simpleDialog,
+                        listDialog: listDialog
+                    };
+                }]);
 }());
diff --git a/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovOverlay.js b/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovOverlay.js
index bf9e484..3a91674 100644
--- a/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovOverlay.js
+++ b/uiref/src/main/resources/app/view/uiRefTopov/uiRefTopovOverlay.js
@@ -53,31 +53,31 @@
 
         activate: function () {
             $log.debug("UI Ref topology overlay ACTIVATED");
+            demo.overlayActive(true);
         },
         deactivate: function () {
-            demo.stopDisplay();
+            demo.overlayActive(false);
             $log.debug("UI Ref topology overlay DEACTIVATED");
         },
 
-        // detail panel button definitions
+        // Detail panel button definitions
+        // NOTE: the callbacks needs to be wrapped in anonymous functions
+        //       to defer the dereferencing of 'demo' to after injection
+        //       of the business logic service API.
         buttons: {
-            foo: {
-                gid: 'chain',
-                tt: 'A FOO action',
-                cb: function() {
-                    demo.deviceDialog();
-                }
-            },
-            bar: {
+            simpleDialog: {
                 gid: '*banner',
-                tt: 'A BAR action',
-                cb: function (data) {
-                    $log.debug('BAR action invoked with data:', data);
-                }
+                tt: 'Simple dialog example',
+                cb: function() { demo.simpleDialog(); }
+            },
+            chainDialog: {
+                gid: 'chain',
+                tt: 'Chained dialogs example',
+                cb: function () { demo.chainedDialogs(); }
             }
         },
 
-        // Key bindings for traffic overlay buttons
+        // Key bindings for topology overlay buttons
         // NOTE: fully qual. button ID is derived from overlay-id and key-name
         keyBindings: {
             0: {
@@ -95,20 +95,20 @@
                 tt: 'Start Link Mode',
                 gid: 'chain'
             },
-            G: {
+            A: {
                 cb: function () { demo.listDialog(); },
-                tt: 'Uses the G key',
+                tt: 'List Dialog',
                 gid: 'crown'
             },
 
+            // defines the order in which the buttons appear on the toolbar
             _keyOrder: [
-                '0', 'V', 'F', 'G'
+                '0', 'V', 'F', 'A'
             ]
         },
 
         hooks: {
-            // hook for handling escape key
-            // Must return true to consume ESC, false otherwise.
+            // hook for handling escape key...
             escape: function () {
                 // Must return true to consume ESC, false otherwise.
                 return demo.stopDisplay();
@@ -116,16 +116,24 @@
 
             // hooks for when the selection changes...
             empty: function () {
+                // selection changed to the empty set
                 selectionCallback('empty');
             },
             single: function (data) {
+                // selection changed to a single node
                 selectionCallback('single', data);
+                // NOTE: the detail buttons to show on the dialog are included
+                //       in the detail data response from the server
             },
             multi: function (selectOrder) {
+                // selection changed to more than one node
                 selectionCallback('multi', selectOrder);
-                tov.addDetailButton('foo');
-                tov.addDetailButton('bar');
+                // NOTE: we have to manually add detail button(s) for a 
+                //       multi-selection
+                tov.addDetailButton('simpleDialog');
             },
+            
+            // hooks for mouse movement over nodes (devices/hosts)...
             mouseover: function (m) {
                 // m has id, class, and type properties
                 $log.debug('mouseover:', m);
@@ -138,6 +146,8 @@
         }
     };
 
+    // just an example callback to log the selection to the console.
+    // usually you would do something more useful.
     function selectionCallback(x, d) {
         $log.debug('Selection callback', x, d);
     }