ONOS-5407: Added details panel for Ports View.

Change-Id: I716a22103f1ab37bc092bad6c672abea479146b7
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);
+                    });
+                };
             }]);
 }());