GUI -- Buttons added to topo and device views that navigate to new flows table view.

Change-Id: Ibea4415d3c1fc717e609aebcd2205d0bba01c96d
diff --git a/web/gui/src/main/webapp/app/fw/nav/nav.js b/web/gui/src/main/webapp/app/fw/nav/nav.js
index ad85d90..36ef599 100644
--- a/web/gui/src/main/webapp/app/fw/nav/nav.js
+++ b/web/gui/src/main/webapp/app/fw/nav/nav.js
@@ -21,7 +21,7 @@
     'use strict';
 
     // injected dependencies
-    var $log;
+    var $log, $location, $window, fs;
 
     // internal state
     var navShown = false;
@@ -52,9 +52,29 @@
         return false;
     }
 
+    function navTo(path, params) {
+        var url;
+        if (!path) {
+            $log.warn('Not a valid navigation path');
+            return null;
+        }
+        $location.url('/' + path);
+
+        if (fs.isO(params)) {
+            $location.search(params);
+        } else if (params !== undefined) {
+            $log.warn('Query params not an object', params);
+        }
+
+        url = $location.absUrl();
+        $log.log('Navigating to ', url);
+        $window.location.href = url;
+    }
+
     angular.module('onosNav', [])
-        .controller('NavCtrl', [
-            '$log', function (_$log_) {
+        .controller('NavCtrl', ['$log',
+
+            function (_$log_) {
                 var self = this;
                 $log = _$log_;
 
@@ -62,15 +82,22 @@
                 $log.log('NavCtrl has been created');
             }
         ])
-        .factory('NavService', ['$log', function (_$log_) {
-            $log = _$log_;
+        .factory('NavService',
+            ['$log', '$location', '$window', 'FnService',
 
-            return {
-                showNav: showNav,
-                hideNav: hideNav,
-                toggleNav: toggleNav,
-                hideIfShown: hideIfShown
-            };
+            function (_$log_, _$location_, _$window_, _fs_) {
+                $log = _$log_;
+                $location = _$location_;
+                $window = _$window_;
+                fs = _fs_;
+
+                return {
+                    showNav: showNav,
+                    hideNav: hideNav,
+                    toggleNav: toggleNav,
+                    hideIfShown: hideIfShown,
+                    navTo: navTo
+                };
         }]);
 
 }());
diff --git a/web/gui/src/main/webapp/app/fw/widget/tooltip.js b/web/gui/src/main/webapp/app/fw/widget/tooltip.js
index 163a01d..d08393c 100644
--- a/web/gui/src/main/webapp/app/fw/widget/tooltip.js
+++ b/web/gui/src/main/webapp/app/fw/widget/tooltip.js
@@ -22,11 +22,11 @@
     'use strict';
 
     // injected references
-    var $log, $timeout, fs;
+    var $log, fs;
 
     // constants
     var hoverHeight = 35,
-        hoverDelay = 500,
+        hoverDelay = 100,
         exitDelay = 100;
 
     // internal state
@@ -104,19 +104,23 @@
         }
     }
 
-    angular.module('onosWidget')
-        .factory('TooltipService', ['$log', '$timeout', 'FnService',
+    function resetTooltip() {
+        tooltip.style('display', 'none').text('');
+    }
 
-        function (_$log_, _$timeout_, _fs_) {
+    angular.module('onosWidget')
+        .factory('TooltipService', ['$log', 'FnService',
+
+        function (_$log_, _fs_) {
             $log = _$log_;
-            $timeout = _$timeout_;
             fs = _fs_;
 
             init();
 
             return {
                 showTooltip: showTooltip,
-                cancelTooltip: cancelTooltip
+                cancelTooltip: cancelTooltip,
+                resetTooltip: resetTooltip
             };
         }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/device/device.css b/web/gui/src/main/webapp/app/view/device/device.css
index 25a6245..a0894bb 100644
--- a/web/gui/src/main/webapp/app/view/device/device.css
+++ b/web/gui/src/main/webapp/app/view/device/device.css
@@ -66,6 +66,14 @@
     margin: 8px 0;
 }
 
+#device-details-panel .top div.left {
+    float: left;
+    padding: 0 18px 0 0;
+}
+#device-details-panel .top div.right {
+    display: inline-block;
+}
+
 #device-details-panel td.label {
     font-style: italic;
     padding-right: 12px;
@@ -73,8 +81,12 @@
     color: #777;
 }
 
-#device-details-panel hr {
-    margin: 12px 0;
+#device-details-panel .actionBtns div {
+    padding: 12px 0;
+}
+#device-details-panel .top hr {
+    width: 95%;
+    margin: 0 auto;
 }
 
 .light #device-details-panel hr {
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 fa4dcf9..236f374 100644
--- a/web/gui/src/main/webapp/app/view/device/device.js
+++ b/web/gui/src/main/webapp/app/view/device/device.js
@@ -22,7 +22,7 @@
     'use strict';
 
     // injected refs
-    var $log, $scope, fs, mast, ps, wss, is;
+    var $log, $scope, fs, mast, ps, wss, is, bns, ns, ttip;
 
     // internal state
     var self,
@@ -36,6 +36,7 @@
         ctnrPdg = 24,
         scrollSize = 17,
         portsTblPdg = 50,
+        flowPath = 'flow',
 
         pName = 'device-details-panel',
         detailsReq = 'deviceDetailsRequest',
@@ -67,7 +68,7 @@
     }
 
     function setUpPanel() {
-        var container, closeBtn;
+        var container, closeBtn, tblDiv;
         detailsPanel.empty();
 
         container = detailsPanel.append('div').classed('container', true);
@@ -77,7 +78,12 @@
         addCloseBtn(closeBtn);
         iconDiv = top.append('div').classed('dev-icon', true);
         top.append('h2');
-        top.append('table');
+
+        tblDiv = top.append('div').classed('top-tables', true);
+        tblDiv.append('div').classed('left', true).append('table');
+        tblDiv.append('div').classed('right', true).append('table');
+
+        top.append('div').classed('actionBtns', true);
         top.append('hr');
 
         bottom = container.append('div').classed('bottom', true);
@@ -95,13 +101,29 @@
         addCell('value', value);
     }
 
-    function populateTop(tbody, details) {
+    function populateTop(tblDiv, btnsDiv, 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').html(details.id);
 
         propOrder.forEach(function (prop, i) {
-            addProp(tbody, i, details[prop]);
+            // properties are split into two tables
+            addProp(i < 3 ? leftTbl : rightTbl, i, details[prop]);
         });
+
+        bns.button(btnsDiv,
+            'dev-dets-p-flows',
+            'flowsTable',
+            function () {
+                ns.navTo(flowPath, { devId: details.id });
+            },
+            'Show flows for this device');
     }
 
     function addPortRow(tbody, port) {
@@ -146,14 +168,15 @@
     }
 
     function populateDetails(details) {
-        var topTb, btmTbl, ports;
+        var topTbs, btnsDiv, btmTbl, ports;
         setUpPanel();
 
-        topTb = top.select('table').append('tbody');
+        topTbs = top.select('.top-tables');
+        btnsDiv = top.select('.actionBtns');
         btmTbl = bottom.select('table');
         ports = details.ports;
 
-        populateTop(topTb, details);
+        populateTop(topTbs, btnsDiv, details);
         populateBottom(btmTbl, ports);
 
         detailsPanel.height(pHeight);
@@ -182,8 +205,10 @@
     .controller('OvDeviceCtrl',
         ['$log', '$scope', 'TableBuilderService', 'FnService',
             'MastService', 'PanelService', 'WebSocketService', 'IconService',
+            'ButtonService', 'NavService', 'TooltipService',
 
-        function (_$log_, _$scope_, tbs, _fs_, _mast_, _ps_, _wss_, _is_) {
+        function (_$log_, _$scope_,
+                  tbs, _fs_, _mast_, _ps_, _wss_, _is_, _bns_, _ns_, _ttip_) {
             $log = _$log_;
             $scope = _$scope_;
             fs = _fs_;
@@ -191,6 +216,9 @@
             ps = _ps_;
             wss = _wss_;
             is = _is_;
+            bns = _bns_;
+            ns = _ns_;
+            ttip = _ttip_;
             self = this;
             var handlers = {};
             self.panelData = [];
@@ -217,12 +245,14 @@
             });
             createDetailsPane();
 
+            // details panel handlers
             handlers[detailsResp] = respDetailsCb;
             wss.bindHandlers(handlers);
 
             $scope.$on('$destroy', function () {
                 ps.destroyPanel(pName);
                 wss.unbindHandlers(handlers);
+                ttip.resetTooltip();
             });
 
             $log.log('OvDeviceCtrl has been created');
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index d65df90..9356c60 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -85,9 +85,6 @@
     top: 320px;
 }
 
-#topo-p-detail .actionBtns {
-    text-align: center;
-}
 #topo-p-detail .actionBtns .actionBtn {
     display: inline-block;
 }
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 68e4d68..8479823 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -30,7 +30,7 @@
 
     // references to injected services etc.
     var $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps,
-        tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs;
+        tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, ttip;
 
     // DOM elements
     var ovtopo, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer;
@@ -319,11 +319,12 @@
             'TopoEventService', 'TopoForceService', 'TopoPanelService',
             'TopoInstService', 'TopoSelectService', 'TopoLinkService',
             'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService',
-            'TopoToolbarService', 'TopoSpriteService',
+            'TopoToolbarService', 'TopoSpriteService', 'TooltipService',
 
         function ($scope, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_,
                   _zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _tes_, _tfs_,
-                  _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_, _ttbs_, tspr) {
+                  _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_, _ttbs_, tspr,
+                  _ttip_) {
             var self = this,
                 projection,
                 dim,
@@ -360,6 +361,7 @@
             tos = _tos_;
             fltr = _fltr_;
             ttbs = _ttbs_;
+            ttip = _ttip_;
 
             self.notifyResize = function () {
                 svgResized(fs.windowSize(mast.mastHeight()));
@@ -373,6 +375,7 @@
                 tis.destroyInst();
                 tfs.destroyForce();
                 ttbs.destroyToolbar();
+                ttip.resetTooltip();
             });
 
             // svg layer and initialization of components
diff --git a/web/gui/src/main/webapp/app/view/topo/topoSelect.js b/web/gui/src/main/webapp/app/view/topo/topoSelect.js
index 44fd14a..cf08d2f 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoSelect.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoSelect.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, fs, wss, tps, tts;
+    var $log, fs, wss, tps, tts, ns;
 
     // api to topoForce
     var api;
@@ -40,6 +40,9 @@
         selectOrder = [],       // the order in which we made selections
         consumeClick = false;   // used to coordinate with SVG click handler
 
+    // constants
+    var flowPath = 'flow';
+
     // ==========================
 
     function nSel() {
@@ -240,6 +243,18 @@
                 tt: 'Show Device Flows'
             });
         }
+        // TODO: have the server return explicit class and ID of each node
+        // for now, we assume the node is a device if it has a URI
+        if ((data.props).hasOwnProperty('URI')) {
+            tps.addAction({
+                id: 'flows-table-btn',
+                gid: 'flowsTable',
+                cb: function () {
+                    ns.navTo(flowPath, { devId: data.id });
+                },
+                tt: 'Show flows for this device'
+            });
+        }
 
         tps.displaySomething();
     }
@@ -264,14 +279,15 @@
     angular.module('ovTopo')
     .factory('TopoSelectService',
         ['$log', 'FnService', 'WebSocketService',
-            'TopoPanelService', 'TopoTrafficService',
+            'TopoPanelService', 'TopoTrafficService', 'NavService',
 
-        function (_$log_, _fs_, _wss_, _tps_, _tts_) {
+        function (_$log_, _fs_, _wss_, _tps_, _tts_, _ns_) {
             $log = _$log_;
             fs = _fs_;
             wss = _wss_;
             tps = _tps_;
             tts = _tts_;
+            ns = _ns_;
 
             function initSelect(_api_) {
                 api = _api_;
diff --git a/web/gui/src/main/webapp/tests/app/fw/mast/mast-spec.js b/web/gui/src/main/webapp/tests/app/fw/mast/mast-spec.js
index 67fbfbb..26ccef8 100644
--- a/web/gui/src/main/webapp/tests/app/fw/mast/mast-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/mast/mast-spec.js
@@ -19,21 +19,18 @@
  */
 describe('Controller: MastCtrl', function () {
     // instantiate the masthead module
-    beforeEach(module('onosMast'));
+    beforeEach(module('onosMast', 'onosUtil'));
 
-    var $log, ctrl, ms;
+    var $log, ctrl, ms, fs;
 
     // we need an instance of the controller
-    beforeEach(inject(function(_$log_, $controller, MastService) {
+    beforeEach(inject(function(_$log_, $controller, MastService, FnService) {
         $log = _$log_;
         ctrl = $controller('MastCtrl');
         ms = MastService;
+        fs = FnService;
     }));
 
-    it('should start with no radio buttons', function () {
-        expect(ctrl.radio).toBeNull();
-    });
-
     it('should declare height to be 36', function () {
         expect(ms.mastHeight()).toBe(36);
     })
diff --git a/web/gui/src/main/webapp/tests/app/fw/nav/nav-spec.js b/web/gui/src/main/webapp/tests/app/fw/nav/nav-spec.js
index 34281ef..d14d514 100644
--- a/web/gui/src/main/webapp/tests/app/fw/nav/nav-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/nav/nav-spec.js
@@ -18,14 +18,29 @@
  ONOS GUI -- Util -- Theme Service - Unit Tests
  */
 describe('factory: fw/nav/nav.js', function() {
-    var ns, $log, fs;
+    var $log, $location, $window, ns, fs;
     var d3Elem;
 
     beforeEach(module('onosNav', 'onosUtil'));
 
-    beforeEach(inject(function (NavService, _$log_, FnService) {
-        ns = NavService;
+    var mockWindow = {
+        location: {
+            href: 'http://server/#/mock/url'
+        }
+    };
+
+    beforeEach(function () {
+        module(function ($provide) {
+            $provide.value('$window', mockWindow);
+        });
+    });
+
+    beforeEach(inject(function (_$log_, _$location_, _$window_,
+                                NavService, FnService) {
         $log = _$log_;
+        $location = _$location_;
+        $window = _$window_;
+        ns = NavService;
         fs = FnService;
         d3Elem = d3.select('body').append('div').attr('id', 'nav');
         ns.hideNav();
@@ -41,7 +56,7 @@
 
     it('should define api functions', function () {
         expect(fs.areFunctions(ns, [
-            'showNav', 'hideNav', 'toggleNav', 'hideIfShown'
+            'showNav', 'hideNav', 'toggleNav', 'hideIfShown', 'navTo'
         ])).toBeTruthy();
     });
 
@@ -95,4 +110,56 @@
         checkHidden(true);
     });
 
+    it('should take correct navTo parameters', function () {
+        spyOn($log, 'warn');
+
+        ns.navTo('foo');
+        expect($log.warn).not.toHaveBeenCalled();
+
+        ns.navTo('bar', { q1: 'thing', q2: 'thing2' });
+        expect($log.warn).not.toHaveBeenCalled();
+
+    });
+
+    it('should check navTo parameter warnings', function () {
+        spyOn($log, 'warn');
+
+        expect(ns.navTo()).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Not a valid navigation path');
+
+        ns.navTo('baz', [1, 2, 3]);
+        expect($log.warn).toHaveBeenCalledWith(
+            'Query params not an object', [1, 2, 3]
+        );
+
+        ns.navTo('zoom', 'not a query param');
+        expect($log.warn).toHaveBeenCalledWith(
+            'Query params not an object', 'not a query param'
+        );
+    });
+
+    it('should verify where the window is navigating', function () {
+        ns.navTo('foo');
+        expect($window.location.href).toBe('http://server/#/foo');
+
+        ns.navTo('bar');
+        expect($window.location.href).toBe('http://server/#/bar');
+
+        ns.navTo('baz', { q1: 'thing1', q2: 'thing2' });
+        expect($window.location.href).toBe(
+            'http://server/#/baz?q1=thing1&q2=thing2'
+        );
+
+        ns.navTo('zip', { q3: 'thing3' });
+        expect($window.location.href).toBe(
+            'http://server/#/zip?q3=thing3'
+        );
+
+        ns.navTo('zoom', {});
+        expect($window.location.href).toBe('http://server/#/zoom');
+
+        ns.navTo('roof', [1, 2, 3]);
+        expect($window.location.href).toBe('http://server/#/roof');
+    });
+
 });
diff --git a/web/gui/src/main/webapp/tests/app/fw/widget/tooltip-spec.js b/web/gui/src/main/webapp/tests/app/fw/widget/tooltip-spec.js
index 0ae1f65..165b51d 100644
--- a/web/gui/src/main/webapp/tests/app/fw/widget/tooltip-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/widget/tooltip-spec.js
@@ -42,7 +42,7 @@
 
     it('should define api functions', function () {
         expect(fs.areFunctions(tts, [
-            'showTooltip', 'cancelTooltip'
+            'showTooltip', 'cancelTooltip', 'resetTooltip'
         ])).toBeTruthy();
     });