ONOS-5407: Added details panel for Ports View.

Change-Id: I716a22103f1ab37bc092bad6c672abea479146b7
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java
index d9ad4b5..205be39 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.device.PortStatistics;
 import org.onosproject.ui.RequestHandler;
@@ -31,6 +32,9 @@
 import java.util.Collection;
 import java.util.List;
 
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+
 
 /**
  * Message handler for port view related messages.
@@ -43,6 +47,12 @@
     private static final String DELTA = "showDelta";
     private static final String NZ = "nzFilter";
 
+    private static final String PORT_DETAILS_REQ = "portDetailsRequest";
+    private static final String PORT_DETAILS_RESP = "portDetailsResponse";
+    private static final String DETAILS = "details";
+    private static final String PORT = "port";
+
+    private static final String DEV_ID = "devId";
     private static final String ID = "id";
     private static final String PKT_RX = "pkt_rx";
     private static final String PKT_TX = "pkt_tx";
@@ -51,6 +61,13 @@
     private static final String PKT_RX_DRP = "pkt_rx_drp";
     private static final String PKT_TX_DRP = "pkt_tx_drp";
     private static final String DURATION = "duration";
+    private static final String SPEED = "speed";
+    private static final String ENABLED = "enabled";
+    private static final String TYPE = "type";
+    private static final String TYPE_IID = "_iconid_type";
+    private static final String PORT_ICON_PREFIX = "portIcon_";
+
+
 
     private static final String[] COL_IDS = {
             ID, PKT_RX, PKT_TX, BYTES_RX, BYTES_TX,
@@ -59,7 +76,10 @@
 
     @Override
     protected Collection<RequestHandler> createRequestHandlers() {
-        return ImmutableSet.of(new PortDataRequest());
+        return ImmutableSet.of(
+                new PortDataRequest(),
+                new DetailRequestHandler()
+        );
     }
 
     // handler for port table requests
@@ -124,4 +144,68 @@
                 .cell(DURATION, stat.durationSec());
         }
     }
+
+    private final class DetailRequestHandler extends RequestHandler {
+        private DetailRequestHandler() {
+            super(PORT_DETAILS_REQ);
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            String id = string(payload, ID);
+            String devId = string(payload, DEV_ID);
+
+            DeviceService deviceService = get(DeviceService.class);
+            Port port = deviceService.getPort(deviceId(devId), portNumber(id));
+
+            ObjectNode data = objectNode();
+
+            data.put(ID, id);
+            data.put(DEV_ID, devId);
+            data.put(TYPE, displayType(port.type()));
+            data.put(SPEED, displaySpeed(port.portSpeed()));
+            data.put(ENABLED, port.isEnabled());
+
+            data.put(TYPE_IID, getIconIdForPortType(port.type()));
+
+            ObjectNode rootNode = objectNode();
+            rootNode.set(DETAILS, data);
+
+            // NOTE: ... an alternate way of getting all the details of an item:
+            // Use the codec context to get a JSON of the port. See ONOS-5976.
+            rootNode.set(PORT, getJsonCodecContext().encode(port, Port.class));
+
+            sendMessage(PORT_DETAILS_RESP, rootNode);
+        }
+
+        private String getIconIdForPortType(Port.Type type) {
+            String typeStr = "DEFAULT";
+            // TODO: consider providing alternate icon ID for different types
+            return PORT_ICON_PREFIX + typeStr;
+            // NOTE: look in icon.js for glyphMapping structure to see which
+            //        glyph will be used for the port type.
+        }
+
+        /**
+         * Returns the port type as a displayable string.
+         *
+         * @param type the port type
+         * @return human readable port type
+         */
+        private String displayType(Port.Type type) {
+            // TODO: consider better display values?
+            return type.toString();
+        }
+
+        /**
+         * Returns port speed as a displayable string.
+         *
+         * @param portSpeed port speed in Mbps
+         * @return human readable port speed
+         */
+        private String displaySpeed(long portSpeed) {
+            // TODO: better conversion between Gbps, Mbps, Kbps, etc.
+            return "" + portSpeed + " Mbps";
+        }
+    }
 }
diff --git a/web/gui/src/main/webapp/app/fw/layer/details-panel.js b/web/gui/src/main/webapp/app/fw/layer/details-panel.js
index cc72a06..39dd26c 100644
--- a/web/gui/src/main/webapp/app/fw/layer/details-panel.js
+++ b/web/gui/src/main/webapp/app/fw/layer/details-panel.js
@@ -84,13 +84,18 @@
         closeBtn.on('click', onClose || function () {});
     }
 
-    function addHeading(icon) {
+    function addHeading(icon, makeEditable) {
         top.append('div').classed('iconDiv ' + icon, true);
-        new EditableTextComponent(top.append('h2'), {
-            scope: options.scope,
-            nameChangeRequest: options.nameChangeRequest,
-            keyBindings: options.keyBindings,
-        });
+
+        if (makeEditable) {
+            new EditableTextComponent(top.append('h2'), {
+                scope: options.scope,
+                nameChangeRequest: options.nameChangeRequest,
+                keyBindings: options.keyBindings,
+            });
+        } else {
+            top.append('h2');   // note: title is inserted later
+        }
     }
 
     function addTable(parent, className) {
diff --git a/web/gui/src/main/webapp/app/fw/svg/icon.js b/web/gui/src/main/webapp/app/fw/svg/icon.js
index f2a6b66..f0c4063 100644
--- a/web/gui/src/main/webapp/app/fw/svg/icon.js
+++ b/web/gui/src/main/webapp/app/fw/svg/icon.js
@@ -58,6 +58,9 @@
         devIcon_SWITCH: 'switch',
         devIcon_ROADM: 'roadm',
         devIcon_OTN: 'otn',
+
+        portIcon_DEFAULT: 'm_ports',
+
         deviceTable: 'switch',
         flowTable: 'flowTable',
         portTable: 'portTable',
diff --git a/web/gui/src/main/webapp/app/view/device/device.js b/web/gui/src/main/webapp/app/view/device/device.js
index 536b789..d0d5707 100644
--- a/web/gui/src/main/webapp/app/view/device/device.js
+++ b/web/gui/src/main/webapp/app/view/device/device.js
@@ -74,7 +74,7 @@
         var top = dps.top();
         var bottom = dps.bottom();
 
-        dps.addHeading('dev-icon');
+        dps.addHeading('dev-icon', true);
         top.append('div').classed('top-content', true);
 
         top.append('hr');
@@ -232,7 +232,6 @@
             $scope.pipeconfTip = 'Show pipeconf view for selected device';
 
             // details panel handlers
-            // handlers[detailsResp] = respDetailsCb;
             handlers[nameChangeResp] = respNameCb;
             wss.bindHandlers(handlers);
 
diff --git a/web/gui/src/main/webapp/app/view/host/host.js b/web/gui/src/main/webapp/app/view/host/host.js
index 37668bb..b95ebc8 100644
--- a/web/gui/src/main/webapp/app/view/host/host.js
+++ b/web/gui/src/main/webapp/app/view/host/host.js
@@ -62,7 +62,7 @@
 
         var top = dps.top();
 
-        dps.addHeading('host-icon');
+        dps.addHeading('host-icon', true);
         top.append('div').classed('top-content', true);
 
         top.append('hr');
diff --git a/web/gui/src/main/webapp/app/view/port/port.css b/web/gui/src/main/webapp/app/view/port/port.css
index 669cc0f..5ed80d3 100644
--- a/web/gui/src/main/webapp/app/view/port/port.css
+++ b/web/gui/src/main/webapp/app/view/port/port.css
@@ -45,3 +45,30 @@
 #ov-port tr.no-data td {
     text-align: center;
 }
+
+#port-details-panel.floatpanel {
+    z-index: 0;
+}
+
+
+#port-details-panel .container {
+    padding: 8px 12px;
+}
+
+#port-details-panel .close-btn {
+    position: absolute;
+    right: 12px;
+    top: 12px;
+    cursor: pointer;
+}
+
+#port-details-panel .port-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#port-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+}
diff --git a/web/gui/src/main/webapp/app/view/port/port.html b/web/gui/src/main/webapp/app/view/port/port.html
index 7a5caa6..a0a02be 100644
--- a/web/gui/src/main/webapp/app/view/port/port.html
+++ b/web/gui/src/main/webapp/app/view/port/port.html
@@ -96,7 +96,9 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat="port in tableData | filter: customFilter(queryFilter, query) track by $index"
+                <tr ng-repeat="port in tableData | filter: customFilter(queryFilter, query) track by $index">
+                    ng-click="selectCallback($event, port)"
+                    ng-class="{selected: port.id === selId}"
                     ng-repeat-complete row-id="{{port.id}}">
                     <td>{{port.id}}</td>
                     <td ng-class="(isDelta() ? 'delta' : 'right')">{{port.pkt_rx}}</td>
@@ -112,4 +114,6 @@
 
     </div>
 
+    <port-details-panel></port-details-panel>
+
 </div>
diff --git a/web/gui/src/main/webapp/app/view/port/port.js b/web/gui/src/main/webapp/app/view/port/port.js
index 915c6d5..be5d6dc 100644
--- a/web/gui/src/main/webapp/app/view/port/port.js
+++ b/web/gui/src/main/webapp/app/view/port/port.js
@@ -22,14 +22,42 @@
     'use strict';
 
     // injected references
-    var $log, $scope, $location, tbs, ns, ps;
+    var $log, $scope, $location, tbs, fs, mast, wss, ns, prefs, dps, is, ps;
 
     var nz = 'nzFilter',
         del = 'showDelta';
 
     // internal state
     var nzFilter = true,
-        showDelta = false;
+        showDelta = false,
+        detailsPanel,
+        pStartY,
+        pHeight,
+        wSize,
+        port;
+
+    // constants
+    var topPdg = 28,
+        dPanelWidth = 480,
+
+        pName = 'port-details-panel',
+        detailsReq = 'portDetailsRequest',
+        detailsResp = 'portDetailsResponse';
+
+
+    var keyBindings = {
+        esc: [closePanel, 'Close the details panel'],
+        _helpFormat: ['esc'],
+    };
+
+    function closePanel() {
+        if (detailsPanel.isVisible()) {
+            $scope.selId = null;
+            detailsPanel.hide();
+            return true;
+        }
+        return false;
+    }
 
     var defaultPortPrefsState = {
         nzFilter: 1,
@@ -40,7 +68,7 @@
 
     function updatePrefsState(what, b) {
         prefsState[what] = b ? 1 : 0;
-        ps.setPrefs('port_prefs', prefsState);
+        prefs.setPrefs('port_prefs', prefsState);
     }
 
     function toggleNZState(b) {
@@ -62,8 +90,8 @@
     }
 
     function restoreConfigFromPrefs() {
-        prefsState = ps.asNumbers(
-            ps.getPrefs('port_prefs', defaultPortPrefsState)
+        prefsState = prefs.asNumbers(
+            prefs.getPrefs('port_prefs', defaultPortPrefsState)
         );
 
         $log.debug('Port - Prefs State:', prefsState);
@@ -71,21 +99,91 @@
         toggleNZState(prefsState.nzFilter);
     }
 
+    function createDetailsPanel() {
+        detailsPanel = dps.create(pName, {
+            width: wSize.width,
+            margin: 0,
+            hideMargin: 0,
+            scope: $scope,
+            keyBindings: keyBindings,
+        });
+
+        dps.setResponse(detailsResp, respDetailsCb);
+
+        $scope.hidePanel = function () { detailsPanel.hide(); };
+    }
+
+    function setUpPanel() {
+        dps.empty();
+        dps.addContainers();
+        dps.addCloseButton(closePanel);
+
+        var top = dps.top();
+
+        dps.addHeading('port-icon');
+        top.append('div').classed('top-content', true);
+
+        top.append('hr');
+    }
+
+    function friendlyPropsList(details) {
+        return {
+            'ID': details['id'],
+            'Device': details['devId'],
+            'Type': details['type'],
+            'Speed': details['speed'],
+            'Enabled': details['enabled'],
+        };
+    }
+
+
+    function populateTop(tblDiv, details) {
+        is.loadEmbeddedIcon(dps.select('.iconDiv'), details._iconid_type, 40);
+        dps.top().select('h2').text(details.devId + ' port ' + details.id);
+        dps.addPropsList(tblDiv, friendlyPropsList(details));
+    }
+
+    function populateDetails(details) {
+        setUpPanel();
+        populateTop(dps.select('.top-content'), details);
+        detailsPanel.height(pHeight);
+        detailsPanel.width(dPanelWidth);
+
+    }
+
+    function respDetailsCb(data) {
+        $scope.panelData = data.details;
+        port = data.port;
+        $scope.$apply();
+    }
+
     angular.module('ovPort', [])
         .controller('OvPortCtrl', [
             '$log', '$scope', '$location',
-            'TableBuilderService', 'NavService', 'PrefsService',
+            'TableBuilderService', 'FnService', 'MastService', 'WebSocketService',
+            'NavService', 'PrefsService', 'DetailsPanelService', 'IconService',
+            'PanelService',
 
-            function (_$log_, _$scope_, _$location_, _tbs_, _ns_, _ps_) {
+            function (_$log_, _$scope_, _$location_,
+                      _tbs_, _fs_, _mast_, _wss_,
+                      _ns_, _prefs_, _dps_, _is_, _ps_) {
                 var params;
                 var tableApi;
                 $log = _$log_;
                 $scope = _$scope_;
                 $location = _$location_;
                 tbs = _tbs_;
+                fs = _fs_;
+                mast = _mast_;
+                wss = _wss_;
                 ns = _ns_;
+                prefs = _prefs_;
+                dps = _dps_;
+                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';
@@ -94,7 +192,6 @@
                 $scope.toggleDeltaTip = 'Toggle port delta statistics';
                 $scope.toggleNZTip = 'Toggle non zero port statistics';
 
-                params = $location.search();
                 if (params.hasOwnProperty('devId')) {
                     $scope.devId = params['devId'];
                 }
@@ -104,10 +201,23 @@
                     showDelta: showDelta,
                 };
 
+                function selCb($event, row) {
+                    if ($scope.selId) {
+                        wss.sendEvent(detailsReq, {
+                            id: row.id,
+                            devId: $scope.devId,
+                        });
+                    } else {
+                        $scope.hidePanel();
+                    }
+                    $log.debug('Got a click on:', row);
+                }
+
                 tableApi = tbs.buildTable({
                     scope: $scope,
                     tag: 'port',
                     query: params,
+                    selCb: selCb,
                 });
 
                 function filterToggleState() {
@@ -204,6 +314,74 @@
                 };
 
                 restoreConfigFromPrefs();
+
+                $scope.$on('$destroy', function () {
+                    dps.destroy();
+                });
+
                 $log.log('OvPortCtrl has been created');
+            }])
+    .directive('portDetailsPanel',
+        ['$rootScope', '$window', '$timeout', 'KeyService',
+            function ($rootScope, $window, $timeout, ks) {
+                return function (scope) {
+                    var unbindWatch;
+
+                    function heightCalc() {
+                        pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height')
+                            + mast.mastHeight() + topPdg;
+                        wSize = fs.windowSize(pStartY);
+                        pHeight = wSize.height;
+                    }
+
+                    function initPanel() {
+                        heightCalc();
+                        createDetailsPanel();
+                    }
+
+                    // Safari has a bug where it renders the fixed-layout table wrong
+                    // if you ask for the window's size too early
+                    if (scope.onos.browser === 'safari') {
+                        $timeout(initPanel);
+                    } else {
+                        initPanel();
+                    }
+                    // create key bindings to handle panel
+                    ks.keyBindings(keyBindings);
+
+                    ks.gestureNotes([
+                        ['click', 'Select a row to show port details'],
+                        ['scroll down', 'See more ports'],
+                    ]);
+
+                    // if the panelData changes
+                    scope.$watch('panelData', function () {
+                        if (!fs.isEmptyObject(scope.panelData)) {
+                            populateDetails(scope.panelData);
+                            detailsPanel.show();
+                        }
+                    });
+
+                    // if the window size changes
+                    unbindWatch = $rootScope.$watchCollection(
+                        function () {
+                            return {
+                                h: $window.innerHeight,
+                                w: $window.innerWidth,
+                            };
+                        }, function () {
+                            if (!fs.isEmptyObject(scope.panelData)) {
+                                heightCalc();
+                                populateDetails(scope.panelData);
+                            }
+                        }
+                    );
+
+                    scope.$on('$destroy', function () {
+                        unbindWatch();
+                        ks.unbindKeys();
+                        ps.destroyPanel(pName);
+                    });
+                };
             }]);
 }());