ONOS-5579 Refactoring Details Panel

Added an Editable Textfield Component
Refactored Device View
Refactored Host View

Change-Id: I7ca423f6c198f8e09b20ed4e57e352de04b797e9
diff --git a/web/gui/src/main/webapp/app/fw/layer/details-panel.css b/web/gui/src/main/webapp/app/fw/layer/details-panel.css
new file mode 100644
index 0000000..1c41835
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/layer/details-panel.css
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
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
new file mode 100644
index 0000000..cc72a06
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/layer/details-panel.js
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+(function () {
+
+    var ps, fs, mast, wss, is, EditableTextComponent;
+
+    var panel,
+        pStartY,
+        wSize,
+        wssHandlers = {},
+        options;
+
+    // Constants
+    var topPdg = 28,
+        defaultLabelWidth = 110,
+        defaultValueWidth = 80;
+
+    // Elements
+    var container,
+        top,
+        bottom;
+
+    function createDetailsPanel(name, _options) {
+        options = _options;
+        scope = options.scope;
+
+        panel = ps.createPanel(name, options);
+
+        calculatePositions();
+
+        panel.el().style({
+            position: 'absolute',
+            top: pStartY + 'px',
+        });
+
+        hide();
+
+        return panel;
+    }
+
+    function calculatePositions() {
+        pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height')
+            + mast.mastHeight() + topPdg;
+        wSize = fs.windowSize(pStartY);
+        pHeight = wSize.height;
+    }
+
+    function hide() {
+        panel.hide();
+    }
+
+    function setResponse(name, callback) {
+        var additionalHandler = {};
+        additionalHandler[name] = callback;
+
+        wss.bindHandlers(additionalHandler);
+        wssHandlers = _.extend({}, wssHandlers, additionalHandler);
+    }
+
+    function addContainers() {
+        container = panel.append('div').classed('container', true);
+        top = container.append('div').classed('top', true);
+        bottom = container.append('div').classed('bottom', true);
+    }
+
+    function addCloseButton(onClose) {
+        var closeBtn = top.append('div').classed('close-btn', true);
+
+        is.loadEmbeddedIcon(closeBtn, 'close', 20);
+        closeBtn.on('click', onClose || function () {});
+    }
+
+    function addHeading(icon) {
+        top.append('div').classed('iconDiv ' + icon, true);
+        new EditableTextComponent(top.append('h2'), {
+            scope: options.scope,
+            nameChangeRequest: options.nameChangeRequest,
+            keyBindings: options.keyBindings,
+        });
+    }
+
+    function addTable(parent, className) {
+        return parent.append('div').classed(className, true).append('table');
+    }
+
+    function addProp(tbody, key, value) {
+        console.log(tbody);
+        var tr = tbody.append('tr');
+
+        function addCell(cls, txt, width) {
+            tr.append('td').attr('class', cls).attr('width', width).text(txt);
+        }
+
+        addCell('label', key + ' :', defaultLabelWidth);
+        addCell('value', value, defaultValueWidth);
+    }
+
+    function addPropsList(el, props) {
+        var tblDiv = el.append('div').classed('top-tables', true);
+        var left = addTable(tblDiv, 'left').append('tbody');
+        var right = addTable(tblDiv, 'right').append('tbody');
+
+        var keys = _.keys(props);
+
+        _.each(props, function (value, key) {
+            var index = keys.indexOf(key);
+
+            if (index < keys.length / 2) {
+                addProp(left, key, value);
+            } else {
+                addProp(right, key, value);
+            }
+        });
+    }
+
+    function empty() {
+        panel.empty();
+    }
+
+    function select(id) {
+        return panel.el().select(id);
+    }
+
+    function destroy() {
+        wss.unbindHandlers(wssHandlers);
+    }
+
+    angular.module('onosLayer')
+        .factory('DetailsPanelService', [
+
+            'PanelService', 'FnService', 'MastService', 'WebSocketService',
+            'IconService', 'EditableTextComponent',
+
+            function (_ps_, _fs_, _mast_, _wss_, _is_, _etc_) {
+
+                ps = _ps_;
+                fs = _fs_;
+                mast = _mast_;
+                wss = _wss_;
+                is = _is_;
+                EditableTextComponent = _etc_;
+
+                return {
+                    create: createDetailsPanel,
+                    setResponse: setResponse,
+
+                    addContainers: addContainers,
+                    addCloseButton: addCloseButton,
+                    addHeading: addHeading,
+                    addPropsList: addPropsList,
+
+                    // Elements
+                    container: function () { return container; },
+                    top: function () { return top; },
+                    bottom: function () { return bottom; },
+                    select: select,
+
+                    empty: empty,
+                    hide: hide,
+                    destroy: destroy,
+                };
+            },
+        ]);
+})();
diff --git a/web/gui/src/main/webapp/app/fw/layer/editable-text.js b/web/gui/src/main/webapp/app/fw/layer/editable-text.js
new file mode 100644
index 0000000..09da1ec
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/layer/editable-text.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+(function () {
+
+    var ks, wss;
+
+    var EditableText = function (el, options) {
+      // constructor
+        this.el = el;
+        this.scope = options.scope;
+        this.options = options;
+
+        this.el.classed('editable clickable', true).on('click', this.onEdit.bind(this));
+        this.editingName = false;
+    };
+
+    EditableText.prototype = {
+
+        bindHandlers: function () {
+            ks.keyBindings({
+                'enter': this.save.bind(this),
+                'esc': [this.cancel.bind(this), 'Close the details panel']
+            });
+        },
+
+        unbindHandlers: function () {
+            ks.unbindKeys();
+
+            if (this.options.keyBindings) {
+                // Reset to original bindings before editable text
+                ks.keyBindings(this.options.keyBindings);
+            }
+        },
+
+        addTextField: function () {
+            return this.el.append('input').classed('name-input', true)
+                .attr('type', 'text')
+                .attr('value', this.scope.panelData.name)[0][0];
+        },
+
+        onEdit: function () {
+            if (!this.editingName) {
+                this.el.classed('editable clickable', false);
+                this.el.text('');
+
+                var el = this.addTextField();
+                el.focus();
+                el.select();
+                this.editingName = true;
+
+                this.bindHandlers();
+
+                ks.enableGlobalKeys(false);
+            }
+        },
+
+        exit: function (name) {
+            this.el.text(name);
+            this.el.classed('editable clickable', true);
+            this.editingName = false;
+            ks.enableGlobalKeys(true);
+            this.unbindHandlers();
+        },
+
+        cancel: function (a, b, ev) {
+
+            if (this.editingName) {
+                this.exit(this.scope.panelData.name);
+                return true;
+            }
+
+            return false;
+        },
+
+        save: function () {
+            var id = this.scope.panelData.id,
+                val,
+                newVal;
+
+            if (this.editingName) {
+                val = this.el.select('input').property('value').trim();
+                newVal = val || id;
+
+                this.exit(newVal);
+                this.scope.panelData.name = newVal;
+                wss.sendEvent(this.options.nameChangeRequest, { id: id, name: val });
+            }
+        },
+    };
+
+
+    angular.module('onosLayer')
+        .factory('EditableTextComponent', [
+
+            'KeyService', 'WebSocketService',
+
+            function (_ks_, _wss_) {
+                ks = _ks_;
+                wss = _wss_;
+
+                return EditableText;
+            },
+        ]);
+
+})();
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 4fbb0df..6bc66d8 100644
--- a/web/gui/src/main/webapp/app/view/device/device.js
+++ b/web/gui/src/main/webapp/app/view/device/device.js
@@ -22,17 +22,14 @@
     'use strict';
 
     // injected refs
-    var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks;
+    var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks, dps;
 
     // internal state
     var detailsPanel,
         pStartY,
         pHeight,
         top,
-        bottom,
-        iconDiv,
         wSize,
-        editingName = false,
         device;
 
     // constants
@@ -40,19 +37,12 @@
         ctnrPdg = 24,
         scrollSize = 17,
         portsTblPdg = 50,
-        defaultLabelWidth = 110,
-        defaultValueWidth  = 80,
 
         pName = 'device-details-panel',
         detailsReq = 'deviceDetailsRequest',
         detailsResp = 'deviceDetailsResponse',
         nameChangeReq = 'deviceNameChangeRequest',
         nameChangeResp = 'deviceNameChangeResponse',
-        friendlyProps = [
-            'URI', 'Type', 'Master ID', 'Chassis ID',
-            'Vendor', 'H/W Version', 'S/W Version', 'Protocol', 'Serial #',
-            'Pipeconf',
-        ],
         portCols = [
             'enabled', 'id', 'speed', 'type', 'elinks_dest', 'name',
         ],
@@ -60,6 +50,11 @@
             'Enabled', 'ID', 'Speed', 'Type', 'Egress Links', 'Name',
         ];
 
+    var keyBindings = {
+        esc: [closePanel, 'Close the details panel'],
+        _helpFormat: ['esc'],
+    };
+
     function closePanel() {
         if (detailsPanel.isVisible()) {
             $scope.selId = null;
@@ -69,119 +64,44 @@
         return false;
     }
 
-    function addCloseBtn(div) {
-        is.loadEmbeddedIcon(div, 'close', 20);
-        div.on('click', closePanel);
-    }
-
-    function exitEditMode(nameH2, name) {
-        nameH2.text(name);
-        nameH2.classed('editable clickable', true);
-        editingName = false;
-        ks.enableGlobalKeys(true);
-    }
-
-    function editNameSave() {
-        var nameH2 = top.select('h2'),
-            id = $scope.panelData.id,
-            val,
-            newVal;
-
-        if (editingName) {
-            val = nameH2.select('input').property('value').trim();
-            newVal = val || id;
-
-            exitEditMode(nameH2, newVal);
-            $scope.panelData.name = newVal;
-            wss.sendEvent(nameChangeReq, { id: id, name: val });
-        }
-    }
-
-    function editNameCancel() {
-        if (editingName) {
-            exitEditMode(top.select('h2'), $scope.panelData.name);
-            return true;
-        }
-        return false;
-    }
-
-    function editName() {
-        var nameH2 = top.select('h2'),
-            tf, el;
-
-        if (!editingName) {
-            nameH2.classed('editable clickable', false);
-            nameH2.text('');
-            tf = nameH2.append('input').classed('name-input', true)
-                .attr('type', 'text')
-                .attr('value', $scope.panelData.name);
-            el = tf[0][0];
-            el.focus();
-            el.select();
-            editingName = true;
-            ks.enableGlobalKeys(false);
-        }
-    }
-
-    function handleEscape() {
-        return editNameCancel() || closePanel();
-    }
-
     function setUpPanel() {
-        var container, closeBtn, tblDiv;
-        detailsPanel.empty();
+        // var container, closeBtn, tblDiv;
 
-        container = detailsPanel.append('div').classed('container', true);
+        dps.empty();
+        dps.addContainers();
+        dps.addCloseButton(closePanel);
 
-        top = container.append('div').classed('top', true);
-        closeBtn = top.append('div').classed('close-btn', true);
-        addCloseBtn(closeBtn);
-        iconDiv = top.append('div').classed('dev-icon', true);
-        top.append('h2').classed('editable clickable', true).on('click', editName);
+        var top = dps.top();
+        var bottom = dps.bottom();
 
-        tblDiv = top.append('div').classed('top-tables', true);
-        tblDiv.append('div').classed('left', true).append('table');
-        tblDiv.append('div').classed('right', true).append('table');
+        dps.addHeading('dev-icon');
+        top.append('div').classed('top-content', true);
 
         top.append('hr');
 
-        bottom = container.append('div').classed('bottom', true);
         bottom.append('h2').classed('ports-title', true).text('Ports');
         bottom.append('table');
     }
 
-    function addProp(tbody, index, value) {
-        var tr = tbody.append('tr');
-
-        function addCell(cls, txt, width) {
-            tr.append('td').attr('class', cls).attr('width', width).text(txt);
-        }
-        addCell('label', friendlyProps[index] + ' :', defaultLabelWidth);
-        addCell('value', value, defaultValueWidth);
+    function friendlyPropsList(details) {
+        return {
+            'URI': device.id,
+            'Type': device.type,
+            'Master ID': details['masterid'],
+            'Chassis ID': details['chassid'],
+            'Vendor': device.mfr,
+            'H/W Version': device.hw,
+            'S/W Version': device.sw,
+            'Protocol': details['protocol'],
+            'Serial #': device.serial,
+            'Pipeconf': details['pipeconf'],
+        };
     }
 
     function populateTop(tblDiv, details) {
-        var leftTbl = tblDiv.select('.left')
-                        .select('table')
-                        .append('tbody'),
-            rightTbl = tblDiv.select('.right')
-                        .select('table')
-                        .append('tbody');
-
-        is.loadEmbeddedIcon(iconDiv, details._iconid_type, 40);
-        top.select('h2').text(details.name);
-
-        // === demonstrate use of JsonCodec object see ONOS-5976
-        addProp(leftTbl, 0, device.id);
-        addProp(leftTbl, 1, device.type);
-        addProp(leftTbl, 2, details['masterid']);
-        addProp(leftTbl, 3, details['chassid']);
-        addProp(leftTbl, 4, device.mfr);
-        addProp(rightTbl, 5, device.hw);
-        addProp(rightTbl, 6, device.sw);
-        addProp(rightTbl, 7, details['protocol']);
-        addProp(rightTbl, 8, device.serial);
-        addProp(rightTbl, 9, details['pipeconf']);
+        is.loadEmbeddedIcon(dps.select('.iconDiv'), details._iconid_type, 40);
+        dps.top().select('h2').text(details.name);
+        dps.addPropsList(tblDiv, friendlyPropsList(details));
     }
 
     function addPortRow(tbody, port) {
@@ -193,6 +113,7 @@
     }
 
     function populateBottom(table, ports) {
+
         var theader = table.append('thead').append('tr'),
             tbody = table.append('tbody'),
             tbWidth, tbHeight;
@@ -200,16 +121,15 @@
         friendlyPortCols.forEach(function (col) {
             theader.append('th').text(col);
         });
+
         ports.forEach(function (port) {
             addPortRow(tbody, port);
         });
 
         tbWidth = fs.noPxStyle(tbody, 'width') + scrollSize;
         tbHeight = pHeight
-                    - (fs.noPxStyle(detailsPanel.el()
-                                        .select('.top'), 'height')
-                    + fs.noPxStyle(detailsPanel.el()
-                                        .select('.ports-title'), 'height')
+                    - (fs.noPxStyle(detailsPanel.el().select('.top'), 'height')
+                    + fs.noPxStyle(detailsPanel.el().select('.ports-title'), 'height')
                     + portsTblPdg);
 
         table.style({
@@ -223,15 +143,14 @@
     }
 
     function populateDetails(details) {
-        var topTbs, btmTbl, ports;
+        var btmTbl, ports;
 
         setUpPanel();
 
-        topTbs = top.select('.top-tables');
-        btmTbl = bottom.select('table');
+        btmTbl = dps.bottom().select('table');
         ports = details.ports;
 
-        populateTop(topTbs, details);
+        populateTop(dps.select('.top-content'), details);
         populateBottom(btmTbl, ports);
 
         detailsPanel.height(pHeight);
@@ -250,18 +169,19 @@
         }
     }
 
-    function createDetailsPane() {
-        detailsPanel = ps.createPanel(pName, {
+    function createDetailsPanel() {
+        detailsPanel = dps.create(pName, {
             width: wSize.width,
             margin: 0,
             hideMargin: 0,
+            scope: $scope,
+            keyBindings: keyBindings,
+            nameChangeRequest: nameChangeReq,
         });
-        detailsPanel.el().style({
-            position: 'absolute',
-            top: pStartY + 'px',
-        });
+
+        dps.setResponse(detailsResp, respDetailsCb);
+
         $scope.hidePanel = function () { detailsPanel.hide(); };
-        detailsPanel.hide();
     }
 
     // Sample functions for detail panel creation
@@ -283,10 +203,10 @@
         ['$log', '$scope', '$location', 'TableBuilderService',
             'TableDetailService', 'FnService',
             'MastService', 'PanelService', 'WebSocketService', 'IconService',
-            'NavService', 'KeyService',
+            'NavService', 'KeyService', 'DetailsPanelService',
 
         function (_$log_, _$scope_, _$location_,
-                  tbs, tds, _fs_, _mast_, _ps_, _wss_, _is_, _ns_, _ks_) {
+                  tbs, tds, _fs_, _mast_, _ps_, _wss_, _is_, _ns_, _ks_, _dps_) {
             var params,
                 handlers = {};
 
@@ -300,6 +220,7 @@
             is = _is_;
             ns = _ns_;
             ks = _ks_;
+            dps = _dps_;
 
             params = $loc.search();
 
@@ -310,7 +231,7 @@
             $scope.meterTip = 'Show meter view for selected device';
 
             // details panel handlers
-            handlers[detailsResp] = respDetailsCb;
+            // handlers[detailsResp] = respDetailsCb;
             handlers[nameChangeResp] = respNameCb;
             wss.bindHandlers(handlers);
 
@@ -352,6 +273,7 @@
             };
 
             $scope.$on('$destroy', function () {
+                dps.destroy();
                 wss.unbindHandlers(handlers);
             });
 
@@ -373,7 +295,7 @@
 
             function initPanel() {
                 heightCalc();
-                createDetailsPane();
+                createDetailsPanel();
             }
 
             // Safari has a bug where it renders the fixed-layout table wrong
@@ -384,11 +306,8 @@
                 initPanel();
             }
             // create key bindings to handle panel
-            ks.keyBindings({
-                enter: editNameSave,
-                esc: [handleEscape, 'Close the details panel'],
-                _helpFormat: ['esc'],
-            });
+            ks.keyBindings(keyBindings);
+
             ks.gestureNotes([
                 ['click', 'Select a row to show device details'],
                 ['scroll down', 'See more devices'],
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 b5bf018..37668bb 100644
--- a/web/gui/src/main/webapp/app/view/host/host.js
+++ b/web/gui/src/main/webapp/app/view/host/host.js
@@ -22,16 +22,14 @@
     'use strict';
 
     // injected refs
-    var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks;
+    var $log, $scope, $loc, fs, mast, ps, wss, is, ns, ks, dps;
 
     // internal state
     var detailsPanel,
         pStartY,
         pHeight,
         top,
-        iconDiv,
-        wSize,
-        editingName = false;
+        wSize;
 
     // constants
     var topPdg = 28,
@@ -41,13 +39,10 @@
         nameChangeReq = 'hostNameChangeRequest',
         nameChangeResp = 'hostNameChangeResponse';
 
-    var propOrder = [
-            'id', 'ip', 'mac', 'vlan', 'configured', 'location',
-        ],
-        friendlyProps = [
-            'Host ID', 'IP Address', 'MAC Address', 'VLAN',
-            'Configured', 'Location',
-        ];
+    var keyBindings = {
+        esc: [closePanel, 'Close the details panel'],
+        _helpFormat: ['esc'],
+    };
 
     function closePanel() {
         if (detailsPanel.isVisible()) {
@@ -58,105 +53,41 @@
         return false;
     }
 
-    function addCloseBtn(div) {
-        is.loadEmbeddedIcon(div, 'close', 20);
-        div.on('click', closePanel);
-    }
-
-    function exitEditMode(nameH2, name) {
-        nameH2.text(name);
-        nameH2.classed('editable clickable', true);
-        editingName = false;
-        ks.enableGlobalKeys(true);
-    }
-
-    function editNameSave() {
-        var nameH2 = top.select('h2'),
-            id = $scope.panelData.id,
-            ip = $scope.panelData.ip,
-            val,
-            newVal;
-
-        if (editingName) {
-            val = nameH2.select('input').property('value').trim();
-            newVal = val || ip;
-
-            exitEditMode(nameH2, newVal);
-            $scope.panelData.name = newVal;
-            wss.sendEvent(nameChangeReq, { id: id, name: val });
-        }
-    }
-
-    function editNameCancel() {
-        if (editingName) {
-            exitEditMode(top.select('h2'), $scope.panelData.name);
-            return true;
-        }
-        return false;
-    }
-
-    function editName() {
-        var nameH2 = top.select('h2'),
-            tf, el;
-
-        if (!editingName) {
-            nameH2.classed('editable clickable', false);
-            nameH2.text('');
-            tf = nameH2.append('input').classed('name-input', true)
-                .attr('type', 'text')
-                .attr('value', $scope.panelData.name);
-            el = tf[0][0];
-            el.focus();
-            el.select();
-            editingName = true;
-            ks.enableGlobalKeys(false);
-        }
-    }
-
-    function handleEscape() {
-        return editNameCancel() || closePanel();
-    }
 
     function setUpPanel() {
-        var container, closeBtn;
-        detailsPanel.empty();
 
-        container = detailsPanel.append('div').classed('container', true);
+        dps.empty();
+        dps.addContainers();
+        dps.addCloseButton(closePanel);
 
-        top = container.append('div').classed('top', true);
-        closeBtn = top.append('div').classed('close-btn', true);
-        addCloseBtn(closeBtn);
-        iconDiv = top.append('div').classed('host-icon', true);
-        top.append('h2').classed('editable clickable', true).on('click', editName);
+        var top = dps.top();
 
-        top.append('div').classed('top-tables', true);
+        dps.addHeading('host-icon');
+        top.append('div').classed('top-content', true);
+
         top.append('hr');
     }
 
-    function addProp(tbody, index, value) {
-        var tr = tbody.append('tr');
-
-        function addCell(cls, txt) {
-            tr.append('td').attr('class', cls).text(txt);
-        }
-        addCell('label', friendlyProps[index] + ' :');
-        addCell('value', value);
+    function friendlyPropsList(details) {
+        return {
+            'Host ID': details.id,
+            'IP Address': details.ip[0],
+            'MAC Address': details.mac,
+            'VLAN': details.vlan,
+            'Configured': details.configured,
+            'Location': details.location,
+        };
     }
 
-    function populateTop(details) {
-        var tab = top.select('.top-tables').append('table').append('tbody');
-
-        is.loadEmbeddedIcon(iconDiv, details._iconid_type, 40);
-        top.select('h2').text(details.name);
-
-        propOrder.forEach(function (prop, i) {
-            addProp(tab, i, details[prop]);
-        });
+    function populateTop(tblDiv, details) {
+        is.loadEmbeddedIcon(dps.select('.iconDiv'), details._iconid_type, 40);
+        dps.top().select('h2').text(details.name);
+        dps.addPropsList(tblDiv, friendlyPropsList(details));
     }
 
     function populateDetails(details) {
         setUpPanel();
-        populateTop(details);
+        populateTop(dps.select('.top-content'), details);
         detailsPanel.height(pHeight);
         // configure width based on content.. for now hardcoded
         detailsPanel.width(400);
@@ -174,18 +105,19 @@
         }
     }
 
-    function createDetailsPane() {
-        detailsPanel = ps.createPanel(pName, {
+    function createDetailsPanel() {
+        detailsPanel = dps.create(pName, {
             width: wSize.width,
             margin: 0,
             hideMargin: 0,
+            scope: $scope,
+            keyBindings: keyBindings,
+            nameChangeRequest: nameChangeReq,
         });
-        detailsPanel.el().style({
-            position: 'absolute',
-            top: pStartY + 'px',
-        });
+
+        dps.setResponse(detailsResp, respDetailsCb);
+
         $scope.hidePanel = function () { detailsPanel.hide(); };
-        detailsPanel.hide();
     }
 
 
@@ -196,12 +128,12 @@
             '$location',
             'TableBuilderService',
             'FnService', 'MastService', 'PanelService', 'WebSocketService',
-            'IconService', 'NavService', 'KeyService',
+            'IconService', 'NavService', 'KeyService', 'DetailsPanelService',
 
         function (_$log_, _$scope_, _$location_,
                   tbs,
                   _fs_, _mast_, _ps_, _wss_,
-                  _is_, _ns_, _ks_) {
+                  _is_, _ns_, _ks_, _dps_) {
 
             var params,
                 handlers = {};
@@ -216,13 +148,13 @@
             is = _is_;
             ns = _ns_;
             ks = _ks_;
+            dps = _dps_;
 
             params = $loc.search();
 
             $scope.panelData = {};
 
             // details panel handlers
-            handlers[detailsResp] = respDetailsCb;
             handlers[nameChangeResp] = respNameCb;
             wss.bindHandlers(handlers);
 
@@ -254,6 +186,7 @@
             };
 
             $scope.$on('$destroy', function () {
+                dps.destroy();
                 wss.unbindHandlers(handlers);
             });
 
@@ -261,7 +194,7 @@
         }])
 
     .directive('hostDetailsPanel',
-    ['$rootScope', '$window', '$timeout', 'KeyService',
+    ['$rootScope', '$window', '$timeout', 'KeyService', 'DetailsPanelService',
     function ($rootScope, $window, $timeout, ks) {
         return function (scope) {
             var unbindWatch;
@@ -275,7 +208,7 @@
 
             function initPanel() {
                 heightCalc();
-                createDetailsPane();
+                createDetailsPanel();
             }
 
             // Safari has a bug where it renders the fixed-layout table wrong
@@ -286,11 +219,7 @@
                 initPanel();
             }
             // create key bindings to handle panel
-            ks.keyBindings({
-                enter: editNameSave,
-                esc: [handleEscape, 'Close the details panel'],
-                _helpFormat: ['esc'],
-            });
+            ks.keyBindings(keyBindings);
             ks.gestureNotes([
                 ['click', 'Select a row to show device details'],
                 ['scroll down', 'See more devices'],