[ONOS-6375] Add detailed view for mapping management app

Change-Id: Ib182e5e394173c98705e8653121c92e602764e20
diff --git a/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java b/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java
index b46e3cd..84532dd 100644
--- a/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java
+++ b/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java
@@ -15,14 +15,19 @@
  */
 package org.onosproject.mapping.web.gui;
 
+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;
 import org.onosproject.mapping.MappingEntry;
 import org.onosproject.mapping.MappingService;
 import org.onosproject.mapping.MappingTreatment;
-import org.onosproject.mapping.MappingValue;
 import org.onosproject.mapping.addresses.MappingAddress;
+import org.onosproject.mapping.instructions.MappingInstruction;
+import org.onosproject.mapping.instructions.MulticastMappingInstruction;
+import org.onosproject.mapping.instructions.MulticastMappingInstruction.MulticastType;
+import org.onosproject.mapping.instructions.UnicastMappingInstruction;
+import org.onosproject.mapping.instructions.UnicastMappingInstruction.UnicastType;
 import org.onosproject.net.DeviceId;
 import org.onosproject.ui.RequestHandler;
 import org.onosproject.ui.UiMessageHandler;
@@ -33,10 +38,11 @@
 import org.onosproject.ui.table.cell.HexLongFormatter;
 
 import java.util.Collection;
-import java.util.List;
 
 import static org.onosproject.mapping.MappingStore.Type.MAP_CACHE;
 import static org.onosproject.mapping.MappingStore.Type.MAP_DATABASE;
+import static org.onosproject.mapping.instructions.MappingInstruction.Type.MULTICAST;
+import static org.onosproject.mapping.instructions.MappingInstruction.Type.UNICAST;
 
 /**
  * Message handler for mapping management view related messages.
@@ -47,7 +53,12 @@
     private static final String MAPPING_DATA_RESP = "mappingDataResponse";
     private static final String MAPPINGS = "mappings";
 
+    private static final String MAPPING_DETAIL_REQ = "mappingDetailsRequest";
+    private static final String MAPPING_DETAIL_RESP = "mappingDetailsResponse";
+    private static final String DETAILS = "details";
+
     private static final String ID = "id";
+    private static final String MAPPING_ID = "mappingId";
     private static final String MAPPING_KEY = "mappingKey";
     private static final String MAPPING_VALUE = "mappingValue";
     private static final String MAPPING_ACTION = "mappingAction";
@@ -55,6 +66,13 @@
     private static final String STATE = "state";
     private static final String DATABASE = "database";
     private static final String CACHE = "cache";
+    private static final String MAPPING_TREATMENTS = "mappingTreatments";
+
+    private static final String MAPPING_ADDRESS = "address";
+    private static final String UNICAST_WEIGHT = "unicastWeight";
+    private static final String UNICAST_PRIORITY = "unicastPriority";
+    private static final String MULTICAST_WEIGHT = "multicastWeight";
+    private static final String MULTICAST_PRIORITY = "multicastPriority";
 
     private static final String COMMA = ", ";
     private static final String OX = "0x";
@@ -68,7 +86,10 @@
 
     @Override
     protected Collection<RequestHandler> createRequestHandlers() {
-        return ImmutableSet.of(new MappingMessageRequest());
+        return ImmutableSet.of(
+                new MappingMessageRequest(),
+                new DetailRequestHandler()
+        );
     }
 
     /**
@@ -99,7 +120,6 @@
             tm.setFormatter(TYPE, EnumFormatter.INSTANCE);
             tm.setFormatter(STATE, EnumFormatter.INSTANCE);
             tm.setFormatter(MAPPING_KEY, new MappingKeyFormatter());
-            tm.setFormatter(MAPPING_VALUE, new MappingValueFormatter());
             return tm;
         }
 
@@ -126,8 +146,7 @@
                     .cell(STATE, mapping.state())
                     .cell(TYPE, type)
                     .cell(MAPPING_ACTION, mapping.value().action())
-                    .cell(MAPPING_KEY, mapping)
-                    .cell(MAPPING_VALUE, mapping);
+                    .cell(MAPPING_KEY, mapping);
         }
     }
 
@@ -144,37 +163,121 @@
             if (address == null) {
                 return NULL_ADDRESS_MSG;
             }
-            StringBuilder sb = new StringBuilder("Mapping address: ");
-            sb.append(address.toString());
 
-            return sb.toString();
+            return address.toString();
         }
     }
 
     /**
-     * A formatter for formatting mapping value.
+     * Handler for detailed mapping message requests.
      */
-    private final class MappingValueFormatter implements CellFormatter {
+    private final class DetailRequestHandler extends RequestHandler {
 
-        @Override
-        public String format(Object value) {
-            MappingEntry mapping = (MappingEntry) value;
-            MappingValue mappingValue = mapping.value();
-            List<MappingTreatment> treatments = mappingValue.treatments();
-
-            StringBuilder sb = new StringBuilder("Treatments: ");
-            formatTreatments(sb, treatments);
-
-            return sb.toString();
+        private DetailRequestHandler() {
+            super(MAPPING_DETAIL_REQ);
         }
 
-        private void formatTreatments(StringBuilder sb,
-                                      List<MappingTreatment> treatments) {
-            if (!treatments.isEmpty()) {
-                for (MappingTreatment t : treatments) {
-                    sb.append(t).append(COMMA);
+        private MappingEntry findMappingById(String mappingId) {
+            MappingService ms = get(MappingService.class);
+            Iterable<MappingEntry> dbEntries = ms.getAllMappingEntries(MAP_DATABASE);
+            Iterable<MappingEntry> cacheEntries = ms.getAllMappingEntries(MAP_CACHE);
+
+            for (MappingEntry entry : dbEntries) {
+                if (entry.id().toString().equals(mappingId)) {
+                    return entry;
                 }
             }
+
+            for (MappingEntry entry : cacheEntries) {
+                if (entry.id().toString().equals(mappingId)) {
+                    return entry;
+                }
+            }
+
+            return null;
+        }
+
+        /**
+         * Generates a node object of a given mapping treatment.
+         *
+         * @param treatment mapping treatment
+         * @return node object
+         */
+        private ObjectNode getTreatmentNode(MappingTreatment treatment) {
+            ObjectNode data = objectNode();
+
+            data.put(MAPPING_ADDRESS, treatment.address().toString());
+
+            for (MappingInstruction instruct : treatment.instructions()) {
+                if (instruct.type() == UNICAST) {
+                    UnicastMappingInstruction unicastInstruct =
+                            (UnicastMappingInstruction) instruct;
+                    if (unicastInstruct.subtype() == UnicastType.WEIGHT) {
+                        data.put(UNICAST_WEIGHT,
+                                ((UnicastMappingInstruction.WeightMappingInstruction)
+                                        unicastInstruct).weight());
+                    }
+                    if (unicastInstruct.subtype() == UnicastType.PRIORITY) {
+                        data.put(UNICAST_PRIORITY,
+                                ((UnicastMappingInstruction.PriorityMappingInstruction)
+                                        unicastInstruct).priority());
+                    }
+                }
+
+                if (instruct.type() == MULTICAST) {
+                    MulticastMappingInstruction multicastInstruct =
+                            (MulticastMappingInstruction) instruct;
+                    if (multicastInstruct.subtype() == MulticastType.WEIGHT) {
+                        data.put(MULTICAST_WEIGHT,
+                                ((MulticastMappingInstruction.WeightMappingInstruction)
+                                        multicastInstruct).weight());
+                    }
+                    if (multicastInstruct.subtype() == MulticastType.PRIORITY) {
+                        data.put(MULTICAST_PRIORITY,
+                                ((MulticastMappingInstruction.PriorityMappingInstruction)
+                                        multicastInstruct).priority());
+                    }
+                }
+
+                // TODO: extension address will be handled later
+            }
+
+            return data;
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            String mappingId = string(payload, MAPPING_ID);
+            String type = string(payload, TYPE);
+            String strippedFlowId = mappingId.replaceAll(OX, EMPTY);
+
+            MappingEntry mapping = findMappingById(strippedFlowId);
+            if (mapping != null) {
+                ArrayNode arrayNode = arrayNode();
+
+                for (MappingTreatment treatment : mapping.value().treatments()) {
+                    arrayNode.add(getTreatmentNode(treatment));
+                }
+
+                ObjectNode detailsNode = objectNode();
+                detailsNode.put(MAPPING_ID, mappingId);
+                detailsNode.put(STATE, mapping.state().name());
+                detailsNode.put(TYPE, type);
+                detailsNode.put(MAPPING_ACTION, mapping.value().action().toString());
+
+                ObjectNode keyNode = objectNode();
+                keyNode.put(MAPPING_ADDRESS, mapping.key().address().toString());
+
+                ObjectNode valueNode = objectNode();
+                valueNode.set(MAPPING_TREATMENTS, arrayNode);
+
+                detailsNode.set(MAPPING_KEY, keyNode);
+                detailsNode.set(MAPPING_VALUE, valueNode);
+
+                ObjectNode rootNode = objectNode();
+                rootNode.set(DETAILS, detailsNode);
+                sendMessage(MAPPING_DETAIL_RESP, rootNode);
+            }
         }
     }
 }
diff --git a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css
index 16f65cd..0c5c611 100644
--- a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css
+++ b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css
@@ -35,3 +35,55 @@
     text-align: left;
     padding-left: 36px;
 }
+
+/* More in generic panel.css */
+
+#mapping-details-panel.floatpanel {
+    z-index: 0;
+}
+
+#mapping-details-panel .container {
+    padding: 8px 12px;
+}
+
+#mapping-details-panel .close-btn {
+    position: absolute;
+    right: 12px;
+    top: 12px;
+    cursor: pointer;
+}
+
+#mapping-details-panel .dev-icon {
+    display: inline-block;
+    padding: 0 6px 0 0;
+    vertical-align: middle;
+}
+
+#mapping-details-panel h2 {
+    display: inline-block;
+    margin: 8px 0;
+}
+
+#mapping-details-panel .top-content table {
+    font-size: 12pt;
+}
+
+#mapping-details-panel td.label {
+    font-weight: bold;
+    text-align: right;
+    padding-right: 6px;
+}
+
+#mapping-details-panel .bottom table {
+    border-spacing: 0;
+}
+
+#mapping-details-panel .bottom th {
+    letter-spacing: 0.02em;
+}
+
+#mapping-details-panel .bottom th,
+#mapping-details-panel .bottom td {
+    padding: 6px 12px;
+    text-align: center;
+}
\ No newline at end of file
diff --git a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html
index 0971144..0572210 100644
--- a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html
+++ b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html
@@ -58,7 +58,7 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat-start="mapping in tableData | filter:queryFilter track by $index"
+                <tr ng-repeat="mapping in tableData | filter:queryFilter track by $index"
                     ng-click="selectCallback($event, mapping)"
                     ng-class="{selected: mapping.id === selId}"
                     ng-repeat-complete row-id="{{mapping.id}}">
@@ -68,10 +68,8 @@
                     <td>{{mapping.mappingKey}}</td>
                     <td>{{mapping.mappingAction}}</td>
                 </tr>
-                <tr row-id="{{mapping.id}}" ng-repeat-end ng-hide="brief">
-                    <td class="mappingValue" colspan="5">{{mapping.mappingValue}}</td>
-                </tr>
             </table>
         </div>
     </div>
+    <mapping-details-panel></mapping-details-panel>
 </div>
diff --git a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js
index f047ebc..bae6b89 100644
--- a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js
+++ b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js
@@ -21,19 +21,205 @@
     'use strict';
 
     // injected references
-    var $log, $scope, $location, tbs;
+    var $log, $scope, $location, fs, tbs, mast, ps, wss, is, ks;
+
+    // internal state
+    var detailsPanel,
+        pStartY,
+        pHeight,
+        top,
+        topTable,
+        keyDiv,
+        valueDiv,
+        topKeyTable,
+        bottomValueTable,
+        iconDiv,
+        nameDiv,
+        wSize;
+
+    // constants
+    var topPdg = 28,
+        ctnrPdg = 24,
+        scrollSize = 17,
+        portsTblPdg = 50,
+        htPdg = 479,
+        wtPdg = 532,
+
+        pName = 'mapping-details-panel',
+        detailsReq = 'mappingDetailsRequest',
+        detailsResp = 'mappingDetailsResponse',
+
+        propOrder = [
+            'mappingId', 'type', 'state', 'mappingAction'
+        ],
+        friendlyProps = [
+            'Mapping ID', 'Store Type', 'State', 'Mapping Action'
+        ],
+
+        treatmentPropOrder = [
+            'address', 'unicastPriority', 'unicastWeight', 'multicastPriority',
+            'multicastWeight'
+        ],
+        treatmentFriendlyProps = [
+            'Address', 'U P', 'U W', 'M P', 'M W'
+        ];
+
+    function closePanel() {
+        if (detailsPanel.isVisible()) {
+            $scope.selId = null;
+            detailsPanel.hide();
+            return true;
+        }
+        return false;
+    }
+
+    function addCloseBtn(div) {
+        is.loadEmbeddedIcon(div, 'close', 20);
+        div.on('click', closePanel);
+    }
+
+    function handleEscape() {
+        return closePanel();
+    }
+
+    function setUpPanel() {
+        var container, closeBtn, tblDiv;
+        detailsPanel.empty();
+
+        container = detailsPanel.append('div').classed('container', true);
+        top = container.append('div').classed('top', true);
+        keyDiv = container.append('div').classed('top', true);
+        valueDiv = container.append('div').classed('bottom', true);
+        closeBtn = top.append('div').classed('close-btn', true);
+        addCloseBtn(closeBtn);
+        iconDiv = top.append('div').classed('dev-icon', true);
+        top.append('h2');
+        topTable = top.append('div').classed('top-content', true)
+            .append('table');
+        top.append('hr');
+        keyDiv.append('h2').html('Mapping Key');
+        topKeyTable = keyDiv.append('div').classed('top-content', true)
+                            .append('table');
+        keyDiv.append('hr');
+        valueDiv.append('h2').html('Mapping Value');
+        bottomValueTable = valueDiv.append('table');
+
+        // TODO: add more details later
+    }
+
+    function addProp(tbody, index, value) {
+        var tr = tbody.append('tr');
+
+        function addCell(cls, txt) {
+            tr.append('td').attr('class', cls).html(txt);
+        }
+        addCell('label', friendlyProps[index] + ' :');
+        addCell('value', value);
+    }
+
+    function populateTable(tbody, label, value) {
+        var tr = tbody.append('tr');
+
+        function addCell(cls, txt) {
+            tr.append('td').attr('class', cls).html(txt);
+        }
+        addCell('label', label + ' :');
+        addCell('value', value);
+    }
+
+    function populateTop(details) {
+        is.loadEmbeddedIcon(iconDiv, 'mappingTable', 40);
+        top.select('h2').html(details.mappingId);
+
+        var tbody = topTable.append('tbody');
+
+        var topKeyTablebody = topKeyTable.append('tbody');
+        var keyObject = details['mappingKey'];
+        var address = keyObject.address;
+
+        var bottomValueTableheader = bottomValueTable.append('thead').append('tr')
+        var bottomValueTablebody = bottomValueTable.append('tbody');
+        var valueObject = details['mappingValue'];
+        var treatments = valueObject.mappingTreatments;
+
+        propOrder.forEach(function (prop, i) {
+            addProp(tbody, i, details[prop]);
+        });
+
+        topKeyTablebody.append('tr').append('td').attr('class', 'value').html(address);
+
+        treatmentFriendlyProps.forEach(function (col) {
+            bottomValueTableheader.append('th').html(col);
+        });
+        treatments.forEach(function (sel) {
+            populateTreatmentTable(bottomValueTablebody, sel);
+        });
+    }
+
+    function populateTreatmentTable(tbody, treatment) {
+        var tr = tbody.append('tr');
+        treatmentPropOrder.forEach(function (prop) {
+            addTreatmentProp(tr, treatment[prop]);
+        });
+    }
+
+    function addTreatmentProp(tr, value) {
+        function addCell(cls, txt) {
+            tr.append('td').attr('class', cls).html(txt);
+        }
+        addCell('value', value);
+    }
+
+    function createDetailsPane() {
+        detailsPanel = ps.createPanel(pName, {
+            width: wSize.width,
+            margin: 0,
+            hideMargin: 0
+        });
+        detailsPanel.el().style({
+            position: 'absolute',
+            top: pStartY + 'px'
+        });
+        $scope.hidePanel = function () { detailsPanel.hide(); };
+        detailsPanel.hide();
+    }
+
+    function populateDetails(details) {
+        setUpPanel();
+        populateTop(details);
+
+        //ToDo add more details
+        detailsPanel.height(pHeight);
+        detailsPanel.width(wtPdg);
+    }
+
+    function respDetailsCb(data) {
+        $log.debug("Got response from server :", data);
+        $scope.panelData = data.details;
+        $scope.$apply();
+    }
 
     angular.module('ovMapping', [])
         .controller('OvMappingCtrl',
-        ['$log', '$scope', '$location', 'TableBuilderService',
+        ['$log', '$scope', '$location', 'FnService', 'TableBuilderService',
+         'MastService', 'PanelService', 'KeyService', 'IconService',
+         'WebSocketService',
 
-        function (_$log_, _$scope_, _$location_, _tbs_) {
-            var params;
+        function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _mast_, _ps_,
+                    _ks_, _is_, _wss_) {
+            var params,
+                handlers = {};
 
             $log = _$log_;
             $scope = _$scope_;
             $location = _$location_;
+            fs = _fs_;
             tbs = _tbs_;
+            mast = _mast_;
+            ps = _ps_;
+            ks = _ks_;
+            is = _is_;
+            wss = _wss_;
 
             params = $location.search();
             if (params.hasOwnProperty('devId')) {
@@ -43,9 +229,90 @@
             tbs.buildTable({
                 scope: $scope,
                 tag: 'mapping',
+                selCb: selCb,
                 query: params
              });
 
+            // details panel handlers
+            handlers[detailsResp] = respDetailsCb;
+            wss.bindHandlers(handlers);
+
+             function selCb($event, row) {
+                 if ($scope.selId) {
+                     wss.sendEvent(detailsReq, {mappingId: row.id, type: row.type});
+                 } else {
+                     $scope.hidePanel();
+                 }
+                 $log.debug('Got a click on:', row);
+             }
+
+             $scope.$on('$destroy', function () {
+                 wss.unbindHandlers(handlers);
+             });
+
             $log.log('OvMappingCtrl has been created');
-        }]);
+        }])
+
+    .directive('mappingDetailsPanel',
+    ['$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();
+                createDetailsPane();
+            }
+
+            // 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({
+                esc: [handleEscape, 'Close the details panel'],
+                _helpFormat: ['esc']
+            });
+            ks.gestureNotes([
+                ['click', 'Select a row to show cluster node details'],
+                ['scroll down', 'See available cluster nodes']
+            ]);
+            // 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();
+                ps.destroyPanel(pName);
+            });
+        };
+    }]);
 }());