GUI -- Added onos.ui.addFloatingPanel() function.
- re-instated detail pane in topo2.js; triggered of non-zero selection state.
- single-select now requests details and displays them in detail pane.
- multi-select WIP.

Change-Id: I300a3dfd4d35abc82f832a172854c6aff50d8cd6
diff --git a/web/gui/src/main/webapp/floatPanel.css b/web/gui/src/main/webapp/floatPanel.css
new file mode 100644
index 0000000..1c5a815
--- /dev/null
+++ b/web/gui/src/main/webapp/floatPanel.css
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * 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 -- Floating Panels -- CSS file
+
+ @author Simon Hunt
+ */
+
+.fpanel {
+    position: absolute;
+    z-index: 100;
+    display: block;
+    top: 10%;
+    width: 280px;
+    right: -300px;
+    opacity: 0;
+    background-color: rgba(255,255,255,0.8);
+
+    padding: 10px;
+    color: black;
+    font-size: 10pt;
+    box-shadow: 2px 2px 16px #777;
+}
+
+/* TODO: light/dark themes */
+.light .fpanel {
+
+}
+.dark .fpanel {
+
+}
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 03272e9..ca5b6a8 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -40,6 +40,7 @@
     <link rel="stylesheet" href="base.css">
     <link rel="stylesheet" href="onos2.css">
     <link rel="stylesheet" href="mast2.css">
+    <link rel="stylesheet" href="floatPanel.css">
 
     <!-- This is where contributed stylesheets get INJECTED -->
     <!-- TODO: replace with template marker and inject refs server-side -->
@@ -62,8 +63,9 @@
         <div id="view">
             <!-- NOTE: views injected here by onos.js -->
         </div>
-        <div id="overlays">
-            <!-- NOTE: overlays injected here, as needed -->
+        <div id="floatPanels">
+            <!-- NOTE: floating panels injected here, as needed -->
+            <!--       see onos.ui.addFloatingPanel             -->
         </div>
         <div id="alerts">
             <!-- NOTE: alert content injected here, as needed -->
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json
new file mode 100644
index 0000000..19d9959
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json
@@ -0,0 +1,22 @@
+{
+  "event": "showDetails",
+  "sid": 9,
+  "payload": {
+    "id": "CA:4B:EE:A4:B0:33/-1",
+    "type": "host",
+    "propOrder": [
+      "MAC",
+      "IP",
+      "-",
+      "Latitude",
+      "Longitude"
+    ],
+    "props": {
+      "MAC": "CA:4B:EE:A4:B0:33",
+      "IP": "[10.0.0.1]",
+      "-": "",
+      "Latitude": null,
+      "Longitude": null
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json
new file mode 100644
index 0000000..8ac1f4f
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json
@@ -0,0 +1,33 @@
+{
+  "event": "showDetails",
+  "sid": 37,
+  "payload": {
+    "id": "of:000000000000000a",
+    "type": "switch",
+    "propOrder": [
+      "Name",
+      "Vendor",
+      "H/W Version",
+      "S/W Version",
+      "Serial Number",
+      "-",
+      "Latitude",
+      "Longitude",
+      "Ports",
+      "-",
+      "Master"
+    ],
+    "props": {
+      "Name": null,
+      "Vendor": "Nicira, Inc.",
+      "H/W Version": "Open vSwitch",
+      "S/W Version": "2.0.1",
+      "Serial Number": "None",
+      "-": "",
+      "Latitude": null,
+      "Longitude": null,
+      "Ports": "5",
+      "Master":"local"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json
new file mode 100644
index 0000000..6c88605
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json
@@ -0,0 +1,9 @@
+{
+  "event": "requestDetails",
+  "sid": 15,
+  "payload": {
+    "id": "of:0000000000000003",
+    "class": "device"
+  }
+}
+
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json
new file mode 100644
index 0000000..2cc1bfa
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json
@@ -0,0 +1,8 @@
+{
+  "event": "requestDetails",
+  "sid": 9,
+  "payload": {
+    "id": "CA:4B:EE:A4:B0:33/-1",
+    "class": "host"
+  }
+}
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index f38b35f..0644c32 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -50,6 +50,7 @@
 
         // internal state
         var views = {},
+            fpanels = {},
             current = {
                 view: null,
                 ctx: '',
@@ -57,7 +58,7 @@
                 theme: settings.theme
             },
             built = false,
-            errorCount = 0,
+            buildErrors = [],
             keyHandler = {
                 globalKeys: {},
                 maskedKeys: {},
@@ -70,7 +71,11 @@
             };
 
         // DOM elements etc.
-        var $view,
+        // TODO: verify existence of following elements...
+        var $view = d3.select('#view'),
+            $floatPanels = d3.select('#floatPanels'),
+            $alerts = d3.select('#alerts'),
+            // note, following elements added programmatically...
             $mastRadio;
 
 
@@ -241,10 +246,22 @@
             setView(view, hash, t);
         }
 
+        function buildError(msg) {
+            buildErrors.push(msg);
+        }
+
         function reportBuildErrors() {
             traceFn('reportBuildErrors');
-            // TODO: validate registered views / nav-item linkage etc.
-            console.log('(no build errors)');
+            var nerr = buildErrors.length,
+                errmsg;
+            if (!nerr) {
+                console.log('(no build errors)');
+            } else {
+                errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
+                    buildErrors.join('\n');
+                doAlert(errmsg);
+                console.error(errmsg);
+            }
         }
 
         // returns the reference if it is a function, null otherwise
@@ -449,22 +466,20 @@
         }
 
         function createAlerts() {
-            var al = d3.select('#alerts')
-                .style('display', 'block');
-            al.append('span')
+            $alerts.style('display', 'block');
+            $alerts.append('span')
                 .attr('class', 'close')
                 .text('X')
                 .on('click', closeAlerts);
-            al.append('pre');
-            al.append('p').attr('class', 'footnote')
+            $alerts.append('pre');
+            $alerts.append('p').attr('class', 'footnote')
                 .text('Press ESCAPE to close');
             alerts.open = true;
             alerts.count = 0;
         }
 
         function closeAlerts() {
-            d3.select('#alerts')
-                .style('display', 'none')
+            $alerts.style('display', 'none')
                 .html('');
             alerts.open = false;
         }
@@ -474,7 +489,7 @@
                 oldContent;
 
             if (alerts.count) {
-                oldContent = d3.select('#alerts pre').html();
+                oldContent = $alerts.select('pre').html();
             }
 
             lines = msg.split('\n');
@@ -485,7 +500,7 @@
                 lines += '\n----\n' + oldContent;
             }
 
-            d3.select('#alerts pre').html(lines);
+            $alerts.select('pre').html(lines);
             alerts.count++;
         }
 
@@ -691,6 +706,53 @@
                 libApi[libName] = api;
             },
 
+            // TODO: implement floating panel as a class
+            // TODO: parameterize position (currently hard-coded to TopRight)
+            /*
+             * Creates div in floating panels block, with the given id.
+             * Returns panel token used to interact with the panel
+             */
+            addFloatingPanel: function (id, position) {
+                var pos = position || 'TR',
+                    el,
+                    fp;
+
+                if (fpanels[id]) {
+                    buildError('Float panel with id "' + id + '" already exists.');
+                    return null;
+                }
+
+                el = $floatPanels.append('div')
+                    .attr('id', id)
+                    .attr('class', 'fpanel');
+
+                fp = {
+                    id: id,
+                    el: el,
+                    pos: pos,
+                    show: function () {
+                        console.log('show pane: ' + id);
+                        el.transition().duration(750)
+                            .style('right', '20px')
+                            .style('opacity', 1);
+                    },
+                    hide: function () {
+                        console.log('hide pane: ' + id);
+                        el.transition().duration(750)
+                            .style('right', '-320px')
+                            .style('opacity', 0);
+                    },
+                    empty: function () {
+                        return el.html('');
+                    },
+                    append: function (what) {
+                        return el.append(what);
+                    }
+                };
+                fpanels[id] = fp;
+                return fp;
+            },
+
             // TODO: it remains to be seen whether we keep this style of docs
             /** @api ui addView( vid, nid, cb )
              * Adds a view to the UI.
@@ -782,7 +844,6 @@
             }
             built = true;
 
-            $view = d3.select('#view');
             $mastRadio = d3.select('#mastRadio');
 
             $(window).on('hashchange', hash);
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 6c0c313..aeaad2d 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -96,3 +96,45 @@
     fill: white;
     stroke: red;
 }
+
+
+/* detail topo-detail pane */
+
+#topo-detail {
+/* gets base CSS from .fpanel in floatPanel.css */
+}
+
+
+#topo-detail h2 {
+    margin: 8px 4px;
+    color: black;
+    vertical-align: middle;
+}
+
+#topo-detail h2 img {
+    height: 32px;
+    padding-right: 8px;
+    vertical-align: middle;
+}
+
+#topo-detail p, table {
+    margin: 4px 4px;
+}
+
+#topo-detail td.label {
+    font-style: italic;
+    color: #777;
+    padding-right: 12px;
+}
+
+#topo-detail td.value {
+
+}
+
+#topo-detail hr {
+    height: 1px;
+    color: #ccc;
+    background-color: #ccc;
+    border: 0;
+}
+
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 24053d8..a23f48d 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -152,7 +152,7 @@
         webSock,
         deviceLabelIndex = 0,
         hostLabelIndex = 0,
-
+        detailPane,
         selectOrder = [],
         selections = {},
 
@@ -192,6 +192,10 @@
 
     function testMe(view) {
         view.alert('test');
+        detailPane.show();
+        setTimeout(function () {
+            detailPane.hide();
+        }, 3000);
     }
 
     function abortIfLive() {
@@ -285,14 +289,6 @@
         view.alert('unpin() callback')
     }
 
-    function requestPath(view) {
-        var payload = {
-            one: selections[selectOrder[0]].obj.id,
-            two: selections[selectOrder[1]].obj.id
-        }
-        sendMessage('requestPath', payload);
-    }
-
     // ==============================
     // Radio Button Callbacks
 
@@ -353,6 +349,7 @@
         removeDevice: stillToImplement,
         removeLink: removeLink,
         removeHost: removeHost,
+        showDetails: showDetails,
         showPath: showPath
     };
 
@@ -463,6 +460,12 @@
         }
     }
 
+    function showDetails(data) {
+        fnTrace('showDetails', data.payload.id);
+        populateDetails(data.payload);
+        detailPane.show();
+    }
+
     function showPath(data) {
         fnTrace('showPath', data.payload.id);
         var links = data.payload.links,
@@ -500,6 +503,32 @@
     }
 
     // ==============================
+    // Out-going messages...
+
+    function getSel(idx) {
+        return selections[selectOrder[idx]];
+    }
+
+    // for now, just a host-to-host intent, (and implicit start-monitoring)
+    function requestPath() {
+        var payload = {
+                one: getSel(0).obj.id,
+                two: getSel(1).obj.id
+            };
+        sendMessage('requestPath', payload);
+    }
+
+    // request details for the selected element
+    function requestDetails() {
+        var data = getSel(0).obj,
+            payload = {
+                id: data.id,
+                class: data.class
+            };
+        sendMessage('requestDetails', payload);
+    }
+
+    // ==============================
     // force layout modification functions
 
     function translate(x, y) {
@@ -1015,6 +1044,8 @@
 
     var sid = 0;
 
+    // TODO: use cache of pending messages (key = sid) to reconcile responses
+
     function sendMessage(evType, payload) {
         var toSend = {
                 event: evType,
@@ -1033,7 +1064,6 @@
         wsTrace('rx', msg);
     }
     function wsTrace(rxtx, msg) {
-
         console.log('[' + rxtx + '] ' + msg);
         // TODO: integrate with trace view
         //if (trace) {
@@ -1062,7 +1092,7 @@
 
         if (meta && n.classed('selected')) {
             deselectObject(obj.id);
-            //flyinPane(null);
+            updateDetailPane();
             return;
         }
 
@@ -1074,17 +1104,16 @@
         selectOrder.push(obj.id);
 
         n.classed('selected', true);
-        //flyinPane(obj);
+        updateDetailPane();
     }
 
     function deselectObject(id) {
         var obj = selections[id];
         if (obj) {
             d3.select(obj.el).classed('selected', false);
-            selections[id] = null;
-            // TODO: use splice to remove element
+            delete selections[id];
         }
-        //flyinPane(null);
+        updateDetailPane();
     }
 
     function deselectAll() {
@@ -1092,10 +1121,10 @@
         node.classed('selected', false);
         selections = {};
         selectOrder = [];
-        //flyinPane(null);
+        updateDetailPane();
     }
 
-    // TODO: this click handler does not get unloaded when the view does
+    // FIXME: this click handler does not get unloaded when the view does
     $('#view').on('click', function(e) {
         if (!$(e.target).closest('.node').length) {
             if (!e.metaKey) {
@@ -1104,6 +1133,66 @@
         }
     });
 
+    // update the state of the detail pane, based on current selections
+    function updateDetailPane() {
+        var nSel = selectOrder.length;
+        if (!nSel) {
+            detailPane.hide();
+        } else if (nSel === 1) {
+            singleSelect();
+        } else {
+            multiSelect();
+        }
+    }
+
+    function singleSelect() {
+        requestDetails();
+        // NOTE: detail pane will be shown from showDetails event.
+    }
+
+    function multiSelect() {
+        // TODO: use detail pane for multi-select view.
+        //detailPane.show();
+    }
+
+    function populateDetails(data) {
+        detailPane.empty();
+
+        var title = detailPane.append("h2"),
+            table = detailPane.append("table"),
+            tbody = table.append("tbody");
+
+        $('<img src="img/' + data.type + '.png">').appendTo(title);
+        $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
+
+        data.propOrder.forEach(function(p) {
+            if (p === '-') {
+                addSep(tbody);
+            } else {
+                addProp(tbody, p, data.props[p]);
+            }
+        });
+
+        function addSep(tbody) {
+            var tr = tbody.append('tr');
+            $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
+        }
+
+        function addProp(tbody, label, value) {
+            var tr = tbody.append('tr');
+
+            tr.append('td')
+                .attr('class', 'label')
+                .text(label + ' :');
+
+            tr.append('td')
+                .attr('class', 'value')
+                .text(value);
+        }
+    }
+
+    // ==============================
+    // Test harness code
 
     function prepareScenario(view, ctx, dbg) {
         var sc = scenario,
@@ -1272,4 +1361,6 @@
         resize: resize
     });
 
+    detailPane = onos.ui.addFloatingPanel('topo-detail');
+
 }(ONOS));