Topo2: ONOS-5640, ONOS-5641 ONOS-5645 Show details for Hosts, Links, Sub-Regions
Added Links panel
Details panel shared between Details, Link, Hosts and Regions
Refactored List content for panel views
Reference to the PanelService Element had a name change
Added a Base UIView to extend future views from
Extend method was being repeated

Change-Id: I3fa070fc5140e98720e47f4b90e3571cb0347596
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 33ad2f6..0d5d4cc 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -413,6 +413,29 @@
         return classes.join(' ');
     }
 
+    function extend(protoProps, staticProps) {
+
+        var parent = this,
+            child;
+
+        child = function () {
+        return parent.apply(this, arguments);
+        };
+
+        angular.extend(child, parent, staticProps);
+
+        // Set the prototype chain to inherit from `parent`, without calling
+        // `parent`'s constructor function and add the prototype properties.
+        child.prototype = angular.extend({}, parent.prototype, protoProps);
+        child.prototype.constructor = child;
+
+        // Set a convenience property in case the parent's prototype is needed
+        // later.
+        child.__super__ = parent.prototype;
+
+        return child;
+    }
+
 
     angular.module('onosUtil')
         .factory('FnService',
@@ -452,7 +475,8 @@
                 addToTrie: addToTrie,
                 removeFromTrie: removeFromTrie,
                 trieLookup: trieLookup,
-                classNames: classNames
+                classNames: classNames,
+                extend: extend
             };
     }]);
 
diff --git a/web/gui/src/main/webapp/app/fw/widget/listBuilder.js b/web/gui/src/main/webapp/app/fw/widget/listBuilder.js
new file mode 100644
index 0000000..113cb00
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/widget/listBuilder.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Widget -- List Service
+ */
+
+(function () {
+    'use strict';
+
+    function addProp(el, label, value) {
+        var tr = el.append('tr'),
+            lab;
+        if (typeof label === 'string') {
+            lab = label.replace(/_/g, ' ');
+        } else {
+            lab = label;
+        }
+
+        function addCell(cls, txt) {
+            tr.append('td').attr('class', cls).html(txt);
+        }
+
+        addCell('label', lab + ' :');
+        addCell('value', value);
+    }
+
+    function addSep(el) {
+        el.append('tr').append('td').attr('colspan', 2).append('hr');
+    }
+
+    function listProps(el, data) {
+        data.propOrder.forEach(function (p) {
+            if (p === '-') {
+                addSep(el);
+            } else {
+                addProp(el, p, data.props[p]);
+            }
+        });
+    }
+
+    angular.module('onosWidget')
+    .factory('ListService', [
+        function () {
+            return listProps;
+        }]);
+}());
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
index 540fe5b..4a7a44a 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
@@ -181,7 +181,7 @@
 }
 
 
-#ov-topo2 svg .node.device.selected .node-container {
+#ov-topo2 svg .node.selected .node-container {
     stroke-width: 2.0;
     stroke: #009fdb;
 }
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
index 11953e1..ab684eb 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
@@ -22,7 +22,8 @@
 (function () {
     'use strict';
 
-    var Model;
+    var Model,
+        extend;
 
     function Collection(models, options) {
 
@@ -91,34 +92,12 @@
         }
     };
 
-    Collection.extend = function (protoProps, staticProps) {
-
-        var parent = this;
-        var child;
-
-        child = function () {
-            return parent.apply(this, arguments);
-        };
-
-        angular.extend(child, parent, staticProps);
-
-        // Set the prototype chain to inherit from `parent`, without calling
-        // `parent`'s constructor function and add the prototype properties.
-        child.prototype = angular.extend({}, parent.prototype, protoProps);
-        child.prototype.constructor = child;
-
-        // Set a convenience property in case the parent's prototype is needed
-        // later.
-        child.__super__ = parent.prototype;
-
-        return child;
-    };
-
     angular.module('ovTopo2')
         .factory('Topo2Collection',
-        ['Topo2Model',
-            function (_Model_) {
+        ['Topo2Model', 'FnService',
+            function (_Model_, fn) {
 
+                Collection.extend = fn.extend;
                 Model = _Model_;
                 return Collection;
             }
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2DetailsPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2DetailsPanel.js
new file mode 100644
index 0000000..3bea0bc
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2DetailsPanel.js
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Topology View Module.
+ Module that displays the details panel for selected nodes
+ */
+
+(function () {
+    'use strict';
+
+    // Injected Services
+    var Panel;
+
+    // Internal State
+    var detailsPanel;
+
+    // configuration
+    var id = 'topo2-p-detail',
+        className = 'topo-p',
+        panelOpts = {
+            width: 260          // summary and detail panel width
+        };
+
+    function getInstance() {
+        if (detailsPanel) {
+            return detailsPanel;
+        }
+
+        var options = angular.extend({}, panelOpts, {
+            class: className
+        });
+
+        detailsPanel = new Panel(id, options);
+        detailsPanel.el.classed(className, true);
+
+        return detailsPanel;
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2DetailsPanelService',
+    ['Topo2PanelService',
+        function (_ps_) {
+
+            Panel = _ps_;
+
+            return getInstance;
+        }
+    ]);
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
index 19d5554..9b82c90 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -74,25 +74,7 @@
                     },
                     onClick: function () {
 
-                        var ev = d3.event;
-
-                        if (ev.shiftKey) {
-                            // TODO: Multi-Select Details Panel
-                            this.set('selected', true);
-                        } else {
-
-                            var s = Boolean(this.get('selected'));
-                            // Clear all selected Items
-                            _.each(this.collection.models, function (m) {
-                                m.set('selected', false);
-                            });
-
-                            this.set('selected', !s);
-                        }
-
-                        var selected = this.collection.filter(function (m) {
-                            return m.get('selected');
-                        });
+                        var selected = this.select(d3.event);
 
                         if (_.isArray(selected) && selected.length > 0) {
                             if (selected.length === 1) {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
index 624b234..741f942 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
@@ -23,17 +23,13 @@
     'use strict';
 
     // Injected Services
-    var Panel, gs, wss, flash, bs, fs, ns;
+    var Panel, gs, wss, flash, bs, fs, ns, listProps;
 
     // Internal State
     var detailsPanel;
 
     // configuration
     var id = 'topo2-p-detail',
-        className = 'topo-p',
-        panelOpts = {
-            width: 260          // summary and detail panel width
-        },
         handlerMap = {
             'showDetails': showDetails
         };
@@ -69,43 +65,7 @@
     function init() {
 
         bindHandlers();
-
-        var options = angular.extend({}, panelOpts, {
-            class: className
-        });
-
-        detailsPanel = new Panel(id, options);
-        detailsPanel.p.classed(className, true);
-    }
-
-    function addProp(tbody, label, value) {
-        var tr = tbody.append('tr'),
-            lab;
-        if (typeof label === 'string') {
-            lab = label.replace(/_/g, ' ');
-        } else {
-            lab = label;
-        }
-
-        function addCell(cls, txt) {
-            tr.append('td').attr('class', cls).html(txt);
-        }
-        addCell('label', lab + ' :');
-        addCell('value', value);
-    }
-
-    function addSep(tbody) {
-        tbody.append('tr').append('td').attr('colspan', 2).append('hr');
-    }
-
-    function listProps(tbody, data) {
-        data.propOrder.forEach(function (p) {
-            if (p === '-') {
-                addSep(tbody);
-            } else {
-                addProp(tbody, p, data.props[p]);
-            }
-        });
+        detailsPanel = Panel();
     }
 
     function addBtnFooter() {
@@ -136,10 +96,6 @@
                     cb: function () { ns.navTo(path, { devId: devId }); }
                 });
             }
-            // TODO: Implement Overlay service
-            // else if (btn = _getButtonDef(id, data)) {
-            //     addAction(btn);
-            // }
         });
     }
 
@@ -196,17 +152,17 @@
     }
 
     function toggle() {
-        var on = detailsPanel.p.toggle(),
+        var on = detailsPanel.el.toggle(),
             verb = on ? 'Show' : 'Hide';
         flash.flash(verb + ' Details Panel');
     }
 
     function show() {
-        detailsPanel.p.show();
+        detailsPanel.el.show();
     }
 
     function hide() {
-        detailsPanel.p.hide();
+        detailsPanel.el.hide();
     }
 
     function destroy() {
@@ -216,9 +172,9 @@
 
     angular.module('ovTopo2')
     .factory('Topo2DeviceDetailsPanel',
-    ['Topo2PanelService', 'GlyphService', 'WebSocketService', 'FlashService',
-    'ButtonService', 'FnService', 'NavService',
-        function (_ps_, _gs_, _wss_, _flash_, _bs_, _fs_, _ns_) {
+    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService',
+    'ButtonService', 'FnService', 'NavService', 'ListService', 
+        function (_ps_, _gs_, _wss_, _flash_, _bs_, _fs_, _ns_, _listService_) {
 
             Panel = _ps_;
             gs = _gs_;
@@ -227,6 +183,7 @@
             bs = _bs_;
             fs = _fs_;
             ns = _ns_;
+            listProps = _listService_;
 
             return {
                 init: init,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
index 3fdbeb5..2fa7beb 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
@@ -48,12 +48,34 @@
     angular.module('ovTopo2')
     .factory('Topo2HostService', [
         'Topo2Collection', 'Topo2NodeModel', 'Topo2ViewService',
-        'IconService', 'Topo2ZoomService',
-        function (_Collection_, _NodeModel_, _t2vs_, is, zs) {
+        'IconService', 'Topo2ZoomService', 'Topo2HostsPanelService', 
+        function (_Collection_, _NodeModel_, _t2vs_, is, zs, t2hds) {
 
             Collection = _Collection_;
 
             Model = _NodeModel_.extend({
+                initialize: function () {
+                    this.super = this.constructor.__super__;
+                    this.super.initialize.apply(this, arguments);
+                },
+                events: {
+                    'click': 'onClick'
+                },
+                onChange: function () {
+                    // Update class names when the model changes
+                    if (this.el) {
+                        this.el.attr('class', this.svgClassName());
+                    }
+                },
+                onClick: function () {
+                    var selected = this.select(d3.select);
+
+                    if (selected.length > 0) {
+                        t2hds.displayPanel(this);
+                    } else {
+                        t2hds.hide();
+                    }
+                },
                 nodeType: 'host',
                 icon: function () {
                     var type = this.get('type');
@@ -117,6 +139,7 @@
                         .attr('text-anchor', 'middle');
 
                     this.setScale();
+                    this.setUpEvents();
                 }
             });
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2HostsPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2HostsPanel.js
new file mode 100644
index 0000000..b131729
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2HostsPanel.js
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+    'use strict';
+
+    // Injected Services
+    var Panel, gs, wss, flash, listProps;
+
+    // Internal State
+    var hostPanel, hostData;
+
+    function init() {
+        hostPanel = Panel();
+    }
+
+    function formatHostData(data) {
+        return {
+            title: data.get('id'),
+            propOrder: ['MAC', 'IP', 'VLAN', '-', 'Latitude', 'Longitude'],
+            props: {
+                '-': '',
+                'MAC': data.get('id'),
+                'IP': data.get('ips')[0],
+                'VLAN': 'None', // TODO
+                'Latitude': data.get('location').lat,
+                'Longitude': data.get('location').lng,
+            }
+        }
+    };
+
+    function displayPanel(data) {
+        init();
+
+        hostData = formatHostData(data);
+        render();
+    }
+
+    function render() {
+        hostPanel.el.show();
+        hostPanel.emptyRegions();
+
+        var svg = hostPanel.appendToHeader('div')
+                .classed('icon', true)
+                .append('svg'),
+            title = hostPanel.appendToHeader('h2'),
+            table = hostPanel.appendToBody('table'),
+            tbody = table.append('tbody');
+
+        title.text(hostData.title);
+        gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
+        listProps(tbody, hostData);
+    }
+
+    function show() {
+        hostPanel.el.show();
+    }
+
+    function hide() {
+        hostPanel.el.hide();
+    }
+
+    function toggle() {
+        var on = hostPanel.el.toggle(),
+            verb = on ? 'Show' : 'Hide';
+        flash.flash(verb + ' host Panel');
+    }
+
+    function destroy() {
+        hostPanel.destroy();
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2HostsPanelService',
+    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
+
+            Panel = _ps_;
+            gs = _gs_;
+            wss = _wss_;
+            flash = _flash_;
+            listProps = _listService_;
+
+            return {
+                displayPanel: displayPanel,
+                init: init,
+                show: show,
+                hide: hide,
+                toggle: toggle,
+                destroy: destroy,
+                isVisible: function () { return hostPanel.isVisible(); }
+            };
+        }
+    ]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
index 419f688..b552a9e 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -22,7 +22,7 @@
 (function () {
     'use strict';
 
-    var $log, Collection, Model, ts, sus, t2zs, t2vs;
+    var $log, Collection, Model, ts, sus, t2zs, t2vs, t2lps, fn;
 
     var linkLabelOffset = '0.35em';
 
@@ -124,6 +124,16 @@
             type: function () {
                 return this.get('type');
             },
+            svgClassName: function () {
+                return fn.classNames('link',
+                    this.nodeType,
+                    this.get('type'),
+                    {
+                        enhanced: this.get('enhanced'),
+                        selected: this.get('selected')
+                    }
+                );
+            },
             expected: function () {
                 // TODO: original code is: (s && s.expected) && (t && t.expected);
                 return true;
@@ -137,6 +147,12 @@
 
                 return (sourceOnline) && (targetOnline);
             },
+            onChange: function () {
+                // Update class names when the model changes
+                if (this.el) {
+                    this.el.attr('class', this.svgClassName());
+                }
+            },
             enhance: function () {
                 var data = [],
                     point;
@@ -145,7 +161,7 @@
                     link.unenhance();
                 });
 
-                this.el.classed('enhanced', true);
+                this.set('enhanced', true);
 
                 if (showPort()) {
                     point = this.locatePortLabel();
@@ -193,9 +209,36 @@
                 }
             },
             unenhance: function () {
-                this.el.classed('enhanced', false);
+                this.set('enhanced', false);
                 d3.select('#topo-portLabels').selectAll('.portLabel').remove();
             },
+            select: function () {
+                var ev = d3.event;
+
+                // TODO: if single selection clear selected devices, hosts, sub-regions
+                var s = Boolean(this.get('selected'));
+                // Clear all selected Items
+                _.each(this.collection.models, function (m) {
+                    m.set('selected', false);
+                });
+
+                this.set('selected', !s);
+
+                var selected = this.collection.filter(function (m) {
+                    return m.get('selected');
+                });
+
+                return selected;
+            },
+            showDetails: function () {
+                var selected = this.select(d3.event);
+
+                if (selected) {
+                    t2lps.displayLink(this);
+                } else {
+                    t2lps.hide();
+                }
+            },
             locatePortLabel: function (src) {
 
                 var offset = 32 / (labelDim * t2zs.scale()),
@@ -259,6 +302,7 @@
                 // from mouse position.
                 this.el.on('mouseover', this.enhance.bind(this));
                 this.el.on('mouseout', this.unenhance.bind(this));
+                this.el.on('click', this.showDetails.bind(this));
 
                 if (this.get('type') === 'hostLink') {
                     // sus.visible(link, api.showHosts());
@@ -277,7 +321,7 @@
 
             },
             update: function () {
-                if (this.el.classed('enhanced')) {
+                if (this.get('enhanced')) {
                     this.enhance();
                 }
             }
@@ -298,9 +342,9 @@
     .factory('Topo2LinkService',
         ['$log', 'Topo2Collection', 'Topo2Model',
         'ThemeService', 'SvgUtilService', 'Topo2ZoomService',
-        'Topo2ViewService',
+        'Topo2ViewService', 'Topo2LinkPanelService', 'FnService',
             function (_$log_, _Collection_, _Model_, _ts_, _sus_,
-                _t2zs_, _t2vs_) {
+                _t2zs_, _t2vs_, _t2lps_, _fn_) {
 
                 $log = _$log_;
                 ts = _ts_;
@@ -309,6 +353,8 @@
                 t2vs = _t2vs_;
                 Collection = _Collection_;
                 Model = _Model_;
+                t2lps = _t2lps_;
+                fn = _fn_;
 
                 return {
                     createLinkCollection: createLinkCollection
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2LinkPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2LinkPanel.js
new file mode 100644
index 0000000..cb65ced
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2LinkPanel.js
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+    'use strict';
+
+    // Injected Services
+    var Panel, gs, wss, flash, listProps;
+
+    // Internal State
+    var linkPanel, linkData;
+
+    function init() {
+        linkPanel = Panel();
+    }
+
+    function formatLinkData(data) {
+
+        var source = data.get('source'),
+            target = data.get('target');
+
+        return {
+            title: 'Link',
+            propOrder: [
+                'Type', '-',
+                'A Type', 'A Id', 'A Label', 'A Port', '-',
+                'B Type', 'B Id', 'B Label', 'B Port'
+            ],
+            props: {
+                '-': '',
+                'Type': data.get('type'),
+                'A Type': source.get('nodeType'),
+                'A Id': source.get('id'),
+                'A Label': 'Label',
+                'A Port': data.get('portA') || '',
+                'B Type': target.get('nodeType'),
+                'B Id': target.get('id'),
+                'B Label': 'Label',
+                'B Port': data.get('portB') || '',
+            }
+        }
+    };
+
+    function displayLink(data) {
+        init();
+
+        linkData = formatLinkData(data);
+        render();
+    }
+
+    function render() {
+        linkPanel.el.show();
+        linkPanel.emptyRegions();
+
+        var svg = linkPanel.appendToHeader('div')
+                .classed('icon', true)
+                .append('svg'),
+            title = linkPanel.appendToHeader('h2'),
+            table = linkPanel.appendToBody('table'),
+            tbody = table.append('tbody');
+
+        title.text(linkData.title);
+        gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
+        listProps(tbody, linkData);
+    }
+
+    function show() {
+        linkPanel.el.show();
+    }
+
+    function hide() {
+        linkPanel.el.hide();
+    }
+
+    function toggle() {
+        var on = linkPanel.el.toggle(),
+            verb = on ? 'Show' : 'Hide';
+        flash.flash(verb + ' Link Panel');
+    }
+
+    function destroy() {
+        wss.unbindHandlers(handlerMap);
+        linkPanel.destroy();
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2LinkPanelService',
+    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
+
+            Panel = _ps_;
+            gs = _gs_;
+            wss = _wss_;
+            flash = _flash_;
+            listProps = _listService_;
+
+            return {
+                displayLink: displayLink,
+                init: init,
+                show: show,
+                hide: hide,
+                toggle: toggle,
+                destroy: destroy,
+                isVisible: function () { return linkPanel.isVisible(); }
+            };
+        }
+    ]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
index b1b2a12..f76456e 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
@@ -22,6 +22,8 @@
 (function () {
     'use strict';
 
+    var extend;
+
     function Model(attributes) {
 
         var attrs = attributes || {};
@@ -118,34 +120,13 @@
         }
     };
 
-    Model.extend = function (protoProps, staticProps) {
-
-        var parent = this;
-        var child;
-
-        child = function () {
-            return parent.apply(this, arguments);
-        };
-
-        angular.extend(child, parent, staticProps);
-
-        // Set the prototype chain to inherit from `parent`, without calling
-        // `parent`'s constructor function and add the prototype properties.
-        child.prototype = angular.extend({}, parent.prototype, protoProps);
-        child.prototype.constructor = child;
-
-        // Set a convenience property in case the parent's prototype is needed
-        // later.
-        child.__super__ = parent.prototype;
-
-        return child;
-    };
-
     angular.module('ovTopo2')
     .factory('Topo2Model', [
-        function () {
+        'FnService',
+        function (fn) {
+            Model.extend = fn.extend;
+            
             return Model;
         }
     ]);
-
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
index 048e2f9..18ad7cc 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -76,6 +76,31 @@
                         'mouseout': 'mouseoutHandler'
                     };
                 },
+                select: function () {
+                    var ev = d3.event;
+
+                    // TODO: if single selection clear selected devices, hosts, sub-regions
+
+                    if (ev.shiftKey) {
+                        // TODO: Multi-Select Details Panel
+                        this.set('selected', true);
+                    } else {
+
+                        var s = Boolean(this.get('selected'));
+                        // Clear all selected Items
+                        _.each(this.collection.models, function (m) {
+                            m.set('selected', false);
+                        });
+
+                        this.set('selected', !s);
+                    }
+
+                    var selected = this.collection.filter(function (m) {
+                        return m.get('selected');
+                    });
+
+                    return selected;
+                },
                 createNode: function () {
                     this.set('svgClass', this.svgClassName());
                     t2nps.positionNode(this);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js b/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
index 3687f6b..686ec96 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
@@ -22,30 +22,24 @@
 (function () {
     'use strict';
 
-    var ps;
+    // Injected Services
+    var flash, ps;
 
-    var Panel = function (id, options) {
-        this.id = id;
-        this.p = ps.createPanel(this.id, options);
-        this.setup();
+    var panel = {
+        initialize: function (id, options) {
+            this.id = id;
+            this.el = ps.createPanel(id, options);
+            this.setup();
 
-        if (options.show) {
-            this.p.show();
-        }
-    };
-
-    Panel.prototype = {
+            if (options.show) {
+                this.el.show();
+            }
+        },
         setup: function () {
-            var panel = this.p;
-            panel.empty();
-
-            panel.append('div').classed('header', true);
-            panel.append('div').classed('body', true);
-            panel.append('div').classed('footer', true);
-
-            this.header = panel.el().select('.header');
-            this.body = panel.el().select('.body');
-            this.footer = panel.el().select('.body');
+            this.el.empty();
+            this.header = this.el.append('div').classed('header', true);
+            this.body = this.el.append('div').classed('body', true);
+            this.footer = this.el.append('div').classed('footer', true);
         },
         appendToHeader: function (x) {
             return this.header.append(x);
@@ -65,15 +59,19 @@
             ps.destroyPanel(this.id);
         },
         isVisible: function () {
-            return this.p.isVisible();
+            return this.el.isVisible();
         }
-    };
+    }
 
     angular.module('ovTopo2')
-    .factory('Topo2PanelService', ['PanelService',
-        function (_ps_) {
+    .factory('Topo2PanelService',
+    ['Topo2UIView', 'FlashService', 'PanelService',
+        function (View, _flash_, _ps_) {
+
+            flash = _flash_;
             ps = _ps_;
-            return Panel;
+
+            return View.extend(panel);
         }
     ]);
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
index bd24bbb..24df147 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -54,8 +54,6 @@
             link.createLink();
         });
 
-        console.log(region.get('id'));
-
         // TEMP Map Zoom
         var regionPanZooms = {
             "(root)": {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
index bb64578..a370746 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
@@ -41,9 +41,9 @@
     angular.module('ovTopo2')
     .factory('Topo2SubRegionService',
         ['WebSocketService', 'Topo2Collection', 'Topo2NodeModel',
-        'ThemeService', 'Topo2ViewService',
+        'ThemeService', 'Topo2ViewService', 'Topo2SubRegionPanelService',
 
-            function (_wss_, _c_, _NodeModel_, _ts_, _t2vs_) {
+            function (_wss_, _c_, _NodeModel_, _ts_, _t2vs_m, _t2srp_) {
 
                 wss = _wss_;
                 Collection = _c_;
@@ -54,13 +54,29 @@
                         this.super.initialize.apply(this, arguments);
                     },
                     events: {
-                        'dblclick': 'navigateToRegion'
+                        'dblclick': 'navigateToRegion',
+                        'click': 'onClick'
+                    },
+                    onChange: function () {
+                        // Update class names when the model changes
+                        if (this.el) {
+                            this.el.attr('class', this.svgClassName());
+                        }
                     },
                     nodeType: 'sub-region',
                     icon: function () {
                         var type = this.get('type');
                         return remappedDeviceTypes[type] || type || 'm_cloud';
                     },
+                    onClick: function () {
+                        var selected = this.select(d3.event);
+
+                        if (selected.length > 0) {
+                            _t2srp_.displayPanel(this);
+                        } else {
+                            _t2srp_.hide();
+                        }
+                    },
                     navigateToRegion: function () {
 
                         if (d3.event.defaultPrevented) return;
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegionPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegionPanel.js
new file mode 100644
index 0000000..b5e49b7
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegionPanel.js
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+    'use strict';
+
+    // Injected Services
+    var Panel, gs, wss, flash, listProps;
+
+    // Internal State
+    var subRegionPanel, subRegionData;
+
+    function init() {
+        subRegionPanel = Panel();
+    }
+
+    function formatSubRegionData(data) {
+        return {
+            title: data.get('name'),
+            propOrder: ['Id', 'Type', '-', 'Number of Devices', 'Number of Hosts'],
+            props: {
+                '-': '',
+                'Id': data.get('id'),
+                'Type': data.get('nodeType'),
+                'Number of Devices': data.get('nDevs'),
+                'Number of Hosts': data.get('nHosts')
+            }
+        }
+    };
+
+    function displayPanel(data) {
+        init();
+        subRegionData = formatSubRegionData(data);
+        render();
+    }
+
+    function render() {
+        subRegionPanel.el.show();
+        subRegionPanel.emptyRegions();
+
+        var svg = subRegionPanel.appendToHeader('div')
+                .classed('icon', true)
+                .append('svg'),
+            title = subRegionPanel.appendToHeader('h2'),
+            table = subRegionPanel.appendToBody('table'),
+            tbody = table.append('tbody');
+
+        title.text(subRegionData.title);
+        gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
+        listProps(tbody, subRegionData);
+    }
+
+    function show() {
+        subRegionPanel.el.show();
+    }
+
+    function hide() {
+        subRegionPanel.el.hide();
+    }
+
+    function toggle() {
+        var on = subRegionPanel.el.toggle(),
+            verb = on ? 'Show' : 'Hide';
+        flash.flash(verb + ' subRegion Panel');
+    }
+
+    function destroy() {
+        subRegionPanel.destroy();
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2SubRegionPanelService',
+    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
+
+            Panel = _ps_;
+            gs = _gs_;
+            wss = _wss_;
+            flash = _flash_;
+            listProps = _listService_;
+
+            return {
+                displayPanel: displayPanel,
+                init: init,
+                show: show,
+                hide: hide,
+                toggle: toggle,
+                destroy: destroy,
+                isVisible: function () { return subRegionPanel.isVisible(); }
+            };
+        }
+    ]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
index 43e92fb..b7e671c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // Injected Services
-    var Panel, gs, wss, flash;
+    var Panel, gs, wss, flash, listProps;
 
     // Internal State
     var summaryPanel, summaryData;
@@ -33,7 +33,7 @@
         className = 'topo-p',
         panelOpts = {
             show: true,
-            width: 260          // summary and detail panel width
+            width: 260 // summary and detail panel width
         },
         handlerMap = {
             showSummary: handleSummaryData
@@ -49,37 +49,7 @@
         });
 
         summaryPanel = new Panel(id, options);
-        summaryPanel.p.classed(className, true);
-    }
-
-    function addProp(tbody, label, value) {
-        var tr = tbody.append('tr'),
-            lab;
-        if (typeof label === 'string') {
-            lab = label.replace(/_/g, ' ');
-        } else {
-            lab = label;
-        }
-
-        function addCell(cls, txt) {
-            tr.append('td').attr('class', cls).html(txt);
-        }
-        addCell('label', lab + ' :');
-        addCell('value', value);
-    }
-
-    function addSep(tbody) {
-        tbody.append('tr').append('td').attr('colspan', 2).append('hr');
-    }
-
-    function listProps(tbody, data) {
-        summaryData.propOrder.forEach(function (p) {
-            if (p === '-') {
-                addSep(tbody);
-            } else {
-                addProp(tbody, p, summaryData.props[p]);
-            }
-        });
+        summaryPanel.el.classed(className, true);
     }
 
     function render() {
@@ -94,7 +64,7 @@
 
         title.text(summaryData.title);
         gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
-        listProps(tbody);
+        listProps(tbody, summaryData);
     }
 
     function handleSummaryData(data) {
@@ -107,7 +77,7 @@
     }
 
     function toggle() {
-        var on = summaryPanel.p.toggle(),
+        var on = summaryPanel.el.toggle(),
             verb = on ? 'Show' : 'Hide';
         flash.flash(verb + ' Summary Panel');
     }
@@ -119,17 +89,17 @@
 
     angular.module('ovTopo2')
     .factory('Topo2SummaryPanelService',
-    ['Topo2PanelService', 'GlyphService', 'WebSocketService', 'FlashService',
-        function (_ps_, _gs_, _wss_, _flash_) {
+    ['Topo2PanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
 
             Panel = _ps_;
             gs = _gs_;
             wss = _wss_;
             flash = _flash_;
+            listProps = _listService_;
 
             return {
                 init: init,
-
                 toggle: toggle,
                 destroy: destroy,
                 isVisible: function () { return summaryPanel.isVisible(); }
diff --git a/web/gui/src/main/webapp/app/view/topo2/uiView.js b/web/gui/src/main/webapp/app/view/topo2/uiView.js
new file mode 100644
index 0000000..8bc01cc
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/uiView.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Base UIView class.
+ A base class for UIViews to extend from
+ */
+
+(function () {
+    'use strict';
+
+    function View(options) {
+        if (options && options.el) {
+            this.el = options.el;
+            this.$el = angular.element(this.el);
+        }
+
+        this.initialize.apply(this, arguments);
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2UIView',
+    ['FnService',
+        function (fn) {
+
+            _.extend(View.prototype, {
+                el: null,
+                empty: function () {
+                    if (this.$el) {
+                        this.$el.empty();
+                    }
+                },
+                destroy: function () {
+                    // TODO: Unbind Events
+                    this.empty();
+                    return this;
+                }
+            });
+
+            View.extend = fn.extend;
+            return View;
+        }
+    ]);
+
+})();
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index 0f91449..9f321f2 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -83,6 +83,7 @@
     <script src="app/fw/widget/button.js"></script>
     <script src="app/fw/widget/tableBuilder.js"></script>
     <script src="app/fw/widget/chartBuilder.js"></script>
+    <script src="app/fw/widget/listBuilder.js"></script>
 
     <script src="app/fw/layer/layer.js"></script>
     <script src="app/fw/layer/panel.js"></script>
@@ -127,20 +128,23 @@
     <link rel="stylesheet" href="app/fw/widget/table-theme.css">
 
     <!-- Under development for Region support. -->
-    <!-- <script src="app/view/topo2/topo2.js"></script>
+    <!--<script src="app/view/topo2/topo2.js"></script>
     <script src="app/view/topo2/topo2Breadcrumb.js"></script>
     <script src="app/view/topo2/topo2Collection.js"></script>
     <script src="app/view/topo2/topo2D3.js"></script>
     <script src="app/view/topo2/topo2Dialog.js"></script>
+    <script src="app/view/topo2/topo2DetailsPanel.js"></script>
     <script src="app/view/topo2/topo2Device.js"></script>
     <script src="app/view/topo2/topo2DeviceDetailsPanel.js"></script>
     <script src="app/view/topo2/topo2Event.js"></script>
     <script src="app/view/topo2/topo2Force.js"></script>
     <script src="app/view/topo2/topo2Host.js"></script>
+    <script src="app/view/topo2/topo2HostsPanel.js"></script>
     <script src="app/view/topo2/topo2Instance.js"></script>
     <script src="app/view/topo2/topo2KeyCommands.js"></script>
     <script src="app/view/topo2/topo2Layout.js"></script>
     <script src="app/view/topo2/topo2Link.js"></script>
+    <script src="app/view/topo2/topo2LinkPanel.js"></script>
     <script src="app/view/topo2/topo2Map.js"></script>
     <script src="app/view/topo2/topo2MapCountryFilters.js"></script>
     <script src="app/view/topo2/topo2MapConfig.js"></script>
@@ -154,9 +158,11 @@
     <script src="app/view/topo2/topo2Select.js"></script>
     <script src="app/view/topo2/topo2SummaryPanel.js"></script>
     <script src="app/view/topo2/topo2SubRegion.js"></script>
+    <script src="app/view/topo2/topo2SubRegionPanel.js"></script>
     <script src="app/view/topo2/topo2Theme.js"></script>
     <script src="app/view/topo2/topo2View.js"></script>
     <script src="app/view/topo2/topo2Zoom.js"></script>
+    <script src="app/view/topo2/uiView.js"></script>
     <link rel="stylesheet" href="app/view/topo2/topo2.css">
     <link rel="stylesheet" href="app/view/topo2/topo2-theme.css">-->
 
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index e535460..bc434ca 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -212,12 +212,12 @@
     it('should define api functions', function () {
         expect(fs.areFunctions(fs, [
             'isF', 'isA', 'isS', 'isO', 'contains',
-            'areFunctions', 'areFunctionsNonStrict', 'windowSize', 
+            'areFunctions', 'areFunctionsNonStrict', 'windowSize',
             'isMobile', 'isChrome', 'isSafari', 'isFirefox',
             'debugOn', 'debug',
             'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
             'eecode', 'noPx', 'noPxStyle', 'endsWith', 'parseBitRate', 'addToTrie', 'removeFromTrie', 'trieLookup',
-            'classNames'
+            'classNames', 'extend'
         ])).toBeTruthy();
     });