GUI -- Implemented Panel Service.

Change-Id: I5e60c6ffa5676bc11f7312681af7bca85b4f8036
diff --git a/web/gui/src/main/webapp/app/fw/layer/panel.css b/web/gui/src/main/webapp/app/fw/layer/panel.css
new file mode 100644
index 0000000..f83d595
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/layer/panel.css
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2014,2015 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 -- Panel Service -- CSS file
+ */
+
+.floatpanel {
+    position: absolute;
+    z-index: 100;
+    display: block;
+    top: 64px;
+    width: 200px;
+    right: -220px;
+    opacity: 0;
+    background-color: rgba(255,255,255,0.8);
+
+    padding: 10px;
+    color: black;
+    font-size: 10pt;
+
+    -moz-border-radius: 6px;
+    border-radius: 6px;
+    box-shadow: 0px 2px 12px #777;
+}
+
+/* TODO: light/dark themes */
+.light .floatpanel {
+
+}
+.dark .floatpanel {
+
+}
diff --git a/web/gui/src/main/webapp/app/fw/layer/panel.js b/web/gui/src/main/webapp/app/fw/layer/panel.js
index 6b85e07..2209487 100644
--- a/web/gui/src/main/webapp/app/fw/layer/panel.js
+++ b/web/gui/src/main/webapp/app/fw/layer/panel.js
@@ -20,46 +20,161 @@
 (function () {
     'use strict';
 
-    var $log;
+    var $log, fs;
 
     var defaultSettings = {
-        position: 'TR',
-        side: 'right',
-        width: 200
+        edge: 'right',
+        width: 200,
+        height: 80,
+        margin: 20,
+        xtnTime: 750
     };
 
+    var panels,
+        panelLayer;
+
+
+    function init() {
+        panelLayer = d3.select('#floatpanels');
+        panelLayer.html('');
+        panels = {};
+    }
+
+    // helpers for panel
+    function noop() {}
+
+    function margin(p) {
+        return p.settings.margin;
+    }
+    function noPx(p, what) {
+        return Number(p.el.style(what).replace(/px$/, ''));
+    }
+    function widthVal(p) {
+        return noPx(p, 'width');
+    }
+    function heightVal(p) {
+        return noPx(p, 'height');
+    }
+    function pxShow(p) {
+        return margin(p) + 'px';
+    }
+    function pxHide(p) {
+        return (-margin(p) - widthVal(p)) + 'px';
+    }
+
+    function makePanel(id, settings) {
+        var p = {
+                id: id,
+                settings: settings,
+                on: false,
+                el: null
+            },
+            api = {
+                show: showPanel,
+                hide: hidePanel,
+                empty: emptyPanel,
+                append: appendPanel,
+                width: panelWidth,
+                height: panelHeight,
+                isVisible: panelIsVisible
+            };
+
+        p.el = panelLayer.append('div')
+            .attr('id', id)
+            .attr('class', 'floatpanel')
+            .style('opacity', 0);
+
+        // has to be called after el is set
+        p.el.style(p.settings.edge, pxHide(p));
+        panelWidth(p.settings.width);
+        panelHeight(p.settings.height);
+
+        panels[id] = p;
+
+        function showPanel(cb) {
+            var endCb = fs.isF(cb) || noop;
+            p.on = true;
+            p.el.transition().duration(p.settings.xtnTime)
+                .each('end', endCb)
+                .style(p.settings.edge, pxShow(p))
+                .style('opacity', 1);
+        }
+
+        function hidePanel(cb) {
+            var endCb = fs.isF(cb) || noop;
+            p.on = false;
+            p.el.transition().duration(p.settings.xtnTime)
+                .each('end', endCb)
+                .style(p.settings.edge, pxHide(p))
+                .style('opacity', 0);
+        }
+
+        function emptyPanel() {
+            return p.el.html('');
+        }
+
+        function appendPanel(what) {
+            return p.el.append(what);
+        }
+
+        function panelWidth(w) {
+            if (w === undefined) {
+                return widthVal(p);
+            }
+            p.el.style('width', w + 'px');
+        }
+
+        function panelHeight(h) {
+            if (h === undefined) {
+                return heightVal(p);
+            }
+            p.el.style('height', h + 'px');
+        }
+
+        function panelIsVisible() {
+            return p.on;
+        }
+
+        return api;
+    }
+
+    function removePanel(id) {
+        panelLayer.select('#' + id).remove();
+        delete panels[id];
+    }
+
     angular.module('onosLayer')
-        .factory('PanelService', ['$log', function (_$log_) {
+        .factory('PanelService', ['$log', 'FnService', function (_$log_, _fs_) {
             $log = _$log_;
+            fs = _fs_;
 
-
-            function createPanel(opts) {
+            function createPanel(id, opts) {
                 var settings = angular.extend({}, defaultSettings, opts);
-
-                function renderPanel() {
-
+                if (!id) {
+                    $log.warn('createPanel: no ID given');
+                    return null;
                 }
-
-                function showPanel() {
-
+                if (panels[id]) {
+                    $log.warn('Panel with ID "' + id + '" already exists');
+                    return null;
                 }
+                $log.debug('creating panel:', id, settings);
+                return makePanel(id, settings);
+            }
 
-                function hidePanel() {
-
+            function destroyPanel(id) {
+                if (panels[id]) {
+                    $log.debug('destroying panel:', id);
+                    removePanel(id);
+                } else {
+                    $log.debug('no panel to destroy:', id);
                 }
-
-                var api = {
-                    render: renderPanel,
-                    show: showPanel,
-                    hide: hidePanel
-                };
-
-                $log.debug('creating panel with settings: ', settings);
-                return api;
             }
 
             return {
-                createPanel: createPanel
+                init: init,
+                createPanel: createPanel,
+                destroyPanel: destroyPanel
             };
         }]);
 
diff --git a/web/gui/src/main/webapp/app/index.html b/web/gui/src/main/webapp/app/index.html
index c26b8f7..beeb2d3 100644
--- a/web/gui/src/main/webapp/app/index.html
+++ b/web/gui/src/main/webapp/app/index.html
@@ -66,6 +66,7 @@
     <link rel="stylesheet" href="common.css">
     <link rel="stylesheet" href="fw/mast/mast.css">
     <link rel="stylesheet" href="fw/svg/icon.css">
+    <link rel="stylesheet" href="fw/layer/panel.css">
     <link rel="stylesheet" href="fw/nav/nav.css">
 
     <!-- This is where contributed javascript will get injected -->
diff --git a/web/gui/src/main/webapp/app/onos.js b/web/gui/src/main/webapp/app/onos.js
index b7be490..23e777c 100644
--- a/web/gui/src/main/webapp/app/onos.js
+++ b/web/gui/src/main/webapp/app/onos.js
@@ -24,12 +24,13 @@
     // define core module dependencies here...
     var coreDependencies = [
         'ngRoute',
-        'onosWidget',
+        'onosMast',
+        'onosNav',
         'onosUtil',
         'onosSvg',
         'onosRemote',
-        'onosMast',
-        'onosNav'
+        'onosLayer',
+        'onosWidget'
     ];
 
     // view IDs.. note the first view listed is loaded at startup
@@ -63,9 +64,9 @@
 
         .controller('OnosCtrl', [
             '$log', '$route', '$routeParams', '$location',
-            'KeyService', 'ThemeService', 'GlyphService',
+            'KeyService', 'ThemeService', 'GlyphService', 'PanelService',
 
-        function ($log, $route, $routeParams, $location, ks, ts, gs) {
+        function ($log, $route, $routeParams, $location, ks, ts, gs, ps) {
             var self = this;
 
             self.$route = $route;
@@ -77,6 +78,7 @@
             ts.init();
             ks.installOn(d3.select('body'));
             gs.init();
+            ps.init();
 
             $log.log('OnosCtrl has been created');
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index 729f006..5addda8 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -28,7 +28,7 @@
     ];
 
     // references to injected services etc.
-    var $log, ks, zs, gs, ms, wss;
+    var $log, ks, zs, gs, ms, wss, ps;
 
     // DOM elements
     var ovtopo, svg, defs, zoomLayer, map;
@@ -170,11 +170,12 @@
     angular.module('ovTopo', moduleDependencies)
 
         .controller('OvTopoCtrl', [
-            '$scope', '$log', '$location',
+            '$scope', '$log', '$location', '$timeout',
             'KeyService', 'ZoomService', 'GlyphService', 'MapService',
-            'WebSocketService',
+            'WebSocketService', 'PanelService',
 
-        function ($scope, _$log_, $loc, _ks_, _zs_, _gs_, _ms_, _wss_) {
+        function ($scope, _$log_, $loc, $timeout,
+                  _ks_, _zs_, _gs_, _ms_, _wss_, _ps_) {
             var self = this;
             $log = _$log_;
             ks = _ks_;
@@ -182,16 +183,18 @@
             gs = _gs_;
             ms = _ms_;
             wss = _wss_;
+            ps = _ps_;
 
             self.notifyResize = function () {
                 svgResized(svg.style('width'), svg.style('height'));
             };
 
+            // Cleanup on destroyed scope..
             $scope.$on('$destroy', function () {
                 $log.log('OvTopoCtrl is saying Buh-Bye!');
-                // TODO: cleanup when the scope is destroyed...
-                //  for example, closing the web socket.
-
+                wsock && wsock.close();
+                wsock = null;
+                ps.destroyPanel('topo-p-summary');
             });
 
             // svg layer and initialization of components
@@ -204,6 +207,12 @@
             setUpMap();
             setUpWebSocket($loc.search().wsport);
 
+            // TODO: remove this temporary code....
+            var p = ps.createPanel('topo-p-summary');
+            p.append('h1').text('Hello World');
+            p.show();
+            $timeout(function () { p.hide(); }, 2000);
+
             $log.log('OvTopoCtrl has been created');
         }]);
 }());
diff --git a/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js b/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js
index bcc462b..1d33e4f 100644
--- a/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/layer/panel-spec.js
@@ -18,16 +18,27 @@
  ONOS GUI -- Layer -- Panel Service - Unit Tests
  */
 describe('factory: fw/layer/panel.js', function () {
-    var $log, fs, ps;
+    var $log, $timeout, fs, ps, d3Elem;
 
     beforeEach(module('onosLayer'));
 
-    beforeEach(inject(function (_$log_, FnService, PanelService) {
+    beforeEach(inject(function (_$log_, _$timeout_, FnService, PanelService) {
         $log = _$log_;
+        $timeout = _$timeout_;
         fs = FnService;
         ps = PanelService;
+        d3Elem = d3.select('body').append('div').attr('id', 'floatpanels');
+        ps.init();
     }));
 
+    afterEach(function () {
+        d3.select('#floatpanels').remove();
+        ps.init();
+    });
+
+    function floatPanelSelection() {
+        return d3Elem.selectAll('.floatpanel');
+    }
 
     it('should define PanelService', function () {
         expect(ps).toBeDefined();
@@ -35,8 +46,134 @@
 
     it('should define api functions', function () {
         expect(fs.areFunctions(ps, [
-            'createPanel'
+            'init', 'createPanel', 'destroyPanel'
         ])).toBeTruthy();
     });
 
+    it('should have no panels to start', function () {
+        expect(floatPanelSelection().size()).toBe(0);
+    });
+
+    it('should log a warning if no ID is given', function () {
+        spyOn($log, 'warn');
+        var p = ps.createPanel();
+        expect(p).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('createPanel: no ID given');
+        expect(floatPanelSelection().size()).toBe(0);
+    });
+
+    it('should create a default panel', function () {
+        spyOn($log, 'warn');
+        spyOn($log, 'debug');
+        var p = ps.createPanel('foo');
+        expect(p).not.toBeNull();
+        expect($log.warn).not.toHaveBeenCalled();
+        expect(floatPanelSelection().size()).toBe(1);
+        expect($log.debug).toHaveBeenCalledWith('creating panel:', 'foo', {
+            edge: 'right',
+            width: 200,
+            height: 80,
+            margin: 20,
+            xtnTime: 750
+        });
+
+        // check basic properties
+        expect(p.width()).toEqual(200);
+        expect(p.isVisible()).toBeFalsy();
+
+        var el = floatPanelSelection();
+        expect(el.style('width')).toEqual('200px');
+    });
+
+    it('should complain when a duplicate ID is used', function () {
+        spyOn($log, 'warn');
+        var p = ps.createPanel('foo');
+        expect(p).not.toBeNull();
+        expect($log.warn).not.toHaveBeenCalled();
+        expect(floatPanelSelection().size()).toBe(1);
+
+        var dup = ps.createPanel('foo');
+        expect(dup).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Panel with ID "foo" already exists');
+        expect(floatPanelSelection().size()).toBe(1);
+    });
+
+    it('should note when there is no panel to destroy', function () {
+        spyOn($log, 'debug');
+        ps.destroyPanel('bar');
+        expect($log.debug).toHaveBeenCalledWith('no panel to destroy:', 'bar')
+    });
+
+    it('should destroy the panel', function () {
+        spyOn($log, 'debug');
+        var p = ps.createPanel('foo');
+        expect(floatPanelSelection().size()).toBe(1);
+
+        ps.destroyPanel('foo');
+        expect($log.debug).toHaveBeenCalledWith('destroying panel:', 'foo')
+        expect(floatPanelSelection().size()).toBe(0);
+    });
+
+    it('should allow alternate settings to be given', function () {
+        spyOn($log, 'debug');
+        var p = ps.createPanel('foo', { width: 250, edge: 'left' });
+        expect($log.debug).toHaveBeenCalledWith('creating panel:', 'foo', {
+            edge: 'left',
+            width: 250,
+            height: 80,
+            margin: 20,
+            xtnTime: 750
+        });
+    });
+
+    it('should show and hide the panel', function () {
+        var p = ps.createPanel('foo', {xtnTime:0});
+        expect(p.isVisible()).toBeFalsy();
+
+        p.show();
+        expect(p.isVisible()).toBeTruthy();
+
+        p.hide();
+        expect(p.isVisible()).toBeFalsy();
+    });
+
+    it('should append content to the panel', function () {
+        var p = ps.createPanel('foo');
+        var span = p.append('span').attr('id', 'thisIsMySpan');
+
+        expect(floatPanelSelection().selectAll('span').attr('id'))
+            .toEqual('thisIsMySpan');
+    });
+
+    it('should remove content on empty', function () {
+        var p = ps.createPanel('voop');
+        p.append('span');
+        p.append('span');
+        p.append('span');
+        expect(floatPanelSelection().selectAll('span').size()).toEqual(3);
+
+        p.empty();
+        expect(floatPanelSelection().selectAll('span').size()).toEqual(0);
+        expect(floatPanelSelection().html()).toEqual('');
+    });
+
+    it('should allow programmatic setting of width', function () {
+        var p = ps.createPanel('whatcha', {width:234});
+        expect(floatPanelSelection().style('width')).toEqual('234px');
+        expect(p.width()).toEqual(234);
+
+        p.width(345);
+        expect(floatPanelSelection().style('width')).toEqual('345px');
+        expect(p.width()).toEqual(345);
+    });
+
+    it('should allow programmatic setting of height', function () {
+        var p = ps.createPanel('ciao', {height:50});
+        expect(floatPanelSelection().style('height')).toEqual('50px');
+        expect(p.height()).toEqual(50);
+
+        p.height(100);
+        expect(floatPanelSelection().style('height')).toEqual('100px');
+        expect(p.height()).toEqual(100);
+    });
 });