ONOS-5409: Added details panel to Meters view

Change-Id: Id0614572f7b9e4233dacbfa908d03973fba42a17
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/MeterViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/MeterViewMessageHandler.java
index 8371353..734551a 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/MeterViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MeterViewMessageHandler.java
@@ -24,6 +24,7 @@
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.meter.Band;
 import org.onosproject.net.meter.Meter;
+import org.onosproject.net.meter.MeterId;
 import org.onosproject.net.meter.MeterService;
 import org.onosproject.ui.RequestHandler;
 import org.onosproject.ui.UiMessageHandler;
@@ -52,12 +53,20 @@
     private static final Set<String> UNSUPPORTED_PROTOCOLS =
             ImmutableSet.of(OF_10, OF_11, OF_12);
 
+    private static final String METER_DETAILS_REQ = "meterDetailsRequest";
+    private static final String METER_DETAILS_RESP = "meterDetailsResponse";
+    private static final String DETAILS = "details";
+
+    private static final String DEV_ID = "devId";
     private static final String ID = "id";
     private static final String APP_ID = "app_id";
     private static final String STATE = "state";
     private static final String PACKETS = "packets";
     private static final String BYTES = "bytes";
     private static final String BANDS = "bands";
+    private static final String BURST = "isBurst";
+    private static final String LIFE = "life";
+    private static final String TYPE_IID = "_iconid_type";
 
     private static final String[] COL_IDS = {
             ID, APP_ID, STATE, PACKETS, BYTES, BANDS
@@ -65,7 +74,10 @@
 
     @Override
     protected Collection<RequestHandler> createRequestHandlers() {
-        return ImmutableSet.of(new MeterDataRequest());
+        return ImmutableSet.of(
+            new MeterDataRequest(),
+            new DetailRequestHandler()
+        );
     }
 
     // handler for meter table requests
@@ -160,4 +172,43 @@
             }
         }
     }
+
+
+    private final class DetailRequestHandler extends RequestHandler {
+        private DetailRequestHandler() {
+            super(METER_DETAILS_REQ);
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            Long id = Long.decode(string(payload, ID));
+            String devId = string(payload, DEV_ID);
+
+            DeviceId deviceId = DeviceId.deviceId(devId);
+            MeterService ms = get(MeterService.class);
+            MeterId meterId = MeterId.meterId(id);
+            Meter meter = ms.getMeter(deviceId, meterId);
+
+            ObjectNode data = objectNode();
+
+            data.put(ID, id);
+            data.put(DEV_ID, devId);
+            data.put(APP_ID, meter.appId().name());
+            data.put(BYTES, meter.bytesSeen());
+            data.put(BURST, meter.isBurst());
+            data.put(LIFE, meter.life());
+            data.put(PACKETS, meter.packetsSeen());
+            data.put(STATE, meter.state().toString());
+
+            data.put(TYPE_IID, "meter");
+
+            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 meter. See ONOS-5976.
+            // TODO: rootNode.set(METER, getJsonCodecContext().encode(meter, Meter.class));
+            sendMessage(METER_DETAILS_RESP, rootNode);
+        }
+    }
 }
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 39dd26c..edc1c5e 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
@@ -103,7 +103,6 @@
     }
 
     function addProp(tbody, key, value) {
-        console.log(tbody);
         var tr = tbody.append('tr');
 
         function addCell(cls, txt, width) {
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 f0c4063..af61e52 100644
--- a/web/gui/src/main/webapp/app/fw/svg/icon.js
+++ b/web/gui/src/main/webapp/app/fw/svg/icon.js
@@ -61,6 +61,8 @@
 
         portIcon_DEFAULT: 'm_ports',
 
+        meter: 'meterTable', // TODO: m_meter icon?
+
         deviceTable: 'switch',
         flowTable: 'flowTable',
         portTable: 'portTable',
diff --git a/web/gui/src/main/webapp/app/view/meter/meter.css b/web/gui/src/main/webapp/app/view/meter/meter.css
index b1abf46..be1b6e7 100644
--- a/web/gui/src/main/webapp/app/view/meter/meter.css
+++ b/web/gui/src/main/webapp/app/view/meter/meter.css
@@ -36,3 +36,29 @@
     padding-left: 36px;
     opacity: 0.65;
 }
+
+#meter-details-panel.floatpanel {
+    z-index: 0;
+}
+
+#meter-details-panel .container {
+    padding: 8px 12px;
+}
+
+#meter-details-panel .close-btn {
+    position: absolute;
+    right: 12px;
+    top: 12px;
+    cursor: pointer;
+}
+
+#meter-details-panel .port-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#meter-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+}
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/app/view/meter/meter.html b/web/gui/src/main/webapp/app/view/meter/meter.html
index ddd17f0..cd54db9 100644
--- a/web/gui/src/main/webapp/app/view/meter/meter.html
+++ b/web/gui/src/main/webapp/app/view/meter/meter.html
@@ -78,6 +78,8 @@
                 </tr>
 
                 <tr ng-repeat-start="meter in tableData | filter:queryFilter  track by $index"
+                    ng-click="selectCallback($event, meter)"
+                    ng-class="{selected: meter.id === selId}"
                     ng-repeat-complete row-id="{{meter.id}}">
                     <td>{{meter.id}}</td>
                     <td>{{meter.app_id}}</td>
@@ -94,4 +96,6 @@
 
     </div>
 
+    <meter-details-panel></meter-details-panel>
+
 </div>
diff --git a/web/gui/src/main/webapp/app/view/meter/meter.js b/web/gui/src/main/webapp/app/view/meter/meter.js
index 32058b6..fb4d15d 100644
--- a/web/gui/src/main/webapp/app/view/meter/meter.js
+++ b/web/gui/src/main/webapp/app/view/meter/meter.js
@@ -21,14 +21,109 @@
     'use strict';
 
     // injected references
-    var $log, $scope, $location, fs, tbs, ns;
+    var $log, $scope, $location, fs, tbs, ns, prefs,
+        fs, mast, wss, ns, dps, is, ps;
+
+    var detailsPanel,
+        pStartY,
+        pHeight,
+        wSize,
+        meter;
+
+    // constants
+    var topPdg = 28,
+        dPanelWidth = 480,
+
+        pName = 'meter-details-panel',
+        detailsReq = 'meterDetailsRequest',
+        detailsResp = 'meterDetailsResponse';
+
+
+    var keyBindings = {
+        esc: [closePanel, 'Close the details panel'],
+        _helpFormat: ['esc'],
+    };
+
+    function closePanel() {
+        if (detailsPanel.isVisible()) {
+            $scope.selId = null;
+            detailsPanel.hide();
+            return true;
+        }
+        return false;
+    }
+
+    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) {
+        $log.debug(details);
+        return {
+            'ID': details['id'],
+            'Device': details['devId'],
+            'App Id': details['app_id'],
+            'Bytes': details['bytes'],
+            'Burst': details['isBurst'],
+            'Packets': details['packets'],
+            'State': details['state'],
+        };
+    }
+
+
+    function populateTop(tblDiv, details) {
+        is.loadEmbeddedIcon(dps.select('.iconDiv'), details._iconid_type, 40);
+        dps.top().select('h2').text(details.devId + ' Meter ' + 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;
+        meter = data.meter;
+        $scope.$apply();
+    }
 
     angular.module('ovMeter', [])
     .controller('OvMeterCtrl',
         ['$log', '$scope', '$location', '$sce',
-            'FnService', 'TableBuilderService', 'NavService',
+            'FnService', 'TableBuilderService', 'NavService', 'PrefsService',
+            'MastService', 'WebSocketService', 'DetailsPanelService', 'IconService',
+            'PanelService',
 
-        function (_$log_, _$scope_, _$location_, $sce, _fs_, _tbs_, _ns_) {
+        function (_$log_, _$scope_, _$location_, $sce,
+                  _fs_, _tbs_, _ns_, _prefs_,
+                  _mast_, _wss_, _dps_, _is_, _ps_) {
             var params;
             $log = _$log_;
             $scope = _$scope_;
@@ -36,6 +131,13 @@
             fs = _fs_;
             tbs = _tbs_;
             ns = _ns_;
+            fs = _fs_;
+            mast = _mast_;
+            wss = _wss_;
+            prefs = _prefs_;
+            dps = _dps_;
+            is = _is_;
+
             $scope.deviceTip = 'Show device table';
             $scope.flowTip = 'Show flow view for this device';
             $scope.portTip = 'Show port view for this device';
@@ -47,10 +149,23 @@
                 $scope.devId = params['devId'];
             }
 
+            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);
+            }
+
             tbs.buildTable({
                 scope: $scope,
                 tag: 'meter',
                 query: params,
+                selCb: selCb,
             });
 
             $scope.$watch('tableData', function () {
@@ -75,7 +190,73 @@
                 },
             });
 
+            $scope.$on('$destroy', function () {
+                dps.destroy();
+            });
 
             $log.log('OvMeterCtrl has been created');
-        }]);
+        }])
+        .directive('meterDetailsPanel',
+            ['$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);
+                        });
+                    };
+                }]);
 }());
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 a0a02be..d35c41a 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,7 @@
                     </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}}">