From 4b66859f8da7e0f247dba1b6e8623cf6d4aaf17c Mon Sep 17 00:00:00 2001 From: Simon Hunt Date: Thu, 29 Jan 2015 17:33:53 -0800 Subject: [PATCH] GUI -- Implemented Instance Panel. - handling addInstance event. Change-Id: Ic98a3291bd37ecf1155dbe1696167d0635a31972 --- web/gui/src/main/webapp/app/fw/layer/panel.js | 7 +- web/gui/src/main/webapp/app/fw/svg/glyph.js | 3 +- web/gui/src/main/webapp/app/fw/svg/svgUtil.js | 7 +- web/gui/src/main/webapp/app/index.html | 1 + .../src/main/webapp/app/view/topo/topo.css | 77 ++++- web/gui/src/main/webapp/app/view/topo/topo.js | 9 +- .../main/webapp/app/view/topo/topoEvent.js | 10 +- .../src/main/webapp/app/view/topo/topoInst.js | 304 ++++++++++++++++++ .../main/webapp/app/view/topo/topoPanel.js | 78 ++--- .../webapp/tests/app/fw/layer/panel-spec.js | 7 + .../webapp/tests/app/fw/svg/glyph-spec.js | 6 +- .../webapp/tests/app/fw/svg/svgUtil-spec.js | 18 +- .../src/test/_karma/ev/migrate/ev_2_onos.json | 14 + 13 files changed, 482 insertions(+), 59 deletions(-) create mode 100644 web/gui/src/main/webapp/app/view/topo/topoInst.js create mode 100644 web/gui/src/test/_karma/ev/migrate/ev_2_onos.json 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 1665626b9d..4df0d7200d 100644 --- a/web/gui/src/main/webapp/app/fw/layer/panel.js +++ b/web/gui/src/main/webapp/app/fw/layer/panel.js @@ -75,7 +75,8 @@ append: appendPanel, width: panelWidth, height: panelHeight, - isVisible: panelIsVisible + isVisible: panelIsVisible, + el: panelEl }; p.el = panelLayer.append('div') @@ -136,6 +137,10 @@ return p.on; } + function panelEl() { + return p.el; + } + return api; } diff --git a/web/gui/src/main/webapp/app/fw/svg/glyph.js b/web/gui/src/main/webapp/app/fw/svg/glyph.js index ca046b5c94..af0ae381bd 100644 --- a/web/gui/src/main/webapp/app/fw/svg/glyph.js +++ b/web/gui/src/main/webapp/app/fw/svg/glyph.js @@ -218,8 +218,7 @@ if (xns) { atr.transform = sus.translate(trans); } - elem.append('use').attr(atr).classed('overlay', ovr); - + return elem.append('use').attr(atr).classed('overlay', ovr); } // ---------------------------------------------------------------------- diff --git a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js index 1f5adc0778..735733e0cc 100644 --- a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js +++ b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js @@ -143,11 +143,16 @@ return 'translate(' + x + ',' + y + ')'; } + function stripPx(s) { + return s.replace(/px$/,''); + } + return { createDragBehavior: createDragBehavior, loadGlow: loadGlow, cat7: cat7, - translate: translate + translate: translate, + stripPx: stripPx }; }]); }()); diff --git a/web/gui/src/main/webapp/app/index.html b/web/gui/src/main/webapp/app/index.html index 738c9679e5..4086ec0496 100644 --- a/web/gui/src/main/webapp/app/index.html +++ b/web/gui/src/main/webapp/app/index.html @@ -78,6 +78,7 @@ + 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 d7e7ec3bae..c313d08e73 100644 --- a/web/gui/src/main/webapp/app/view/topo/topo.css +++ b/web/gui/src/main/webapp/app/view/topo/topo.css @@ -43,7 +43,7 @@ } -/* --- Topo Panels --- */ +/* --- Topo Summary Panel --- */ #topo-p-summary { /* Base css from panel.css */ @@ -107,3 +107,78 @@ background-color: #888; color: #888; } + + +/* --- Topo Detail Panel --- */ + +/* TODO: add CSS rules */ + + +/* --- Topo Instance Panel --- */ + +#topo-p-instance { + height: 100px; +} + +#topo-p-instance div.onosInst { + display: inline-block; + width: 170px; + height: 85px; + cursor: pointer; +} + +#topo-p-instance svg rect { + fill: #ccc; + stroke: #aaa; + stroke-width: 3.5; +} +#topo-p-instance .online svg rect { + opacity: 1; + fill: #9cf; + stroke: #555; +} + +#topo-p-instance svg .glyph { + fill: #888; + fill-rule: evenodd; +} +#topo-p-instance .online svg .glyph { + fill: #000; +} + +#topo-p-instance svg .badgeIcon { + fill: #777; + fill-rule: evenodd; +} + +#topo-p-instance .online svg .badgeIcon { + fill: #fff; +} + +#topo-p-instance svg text { + text-anchor: middle; + fill: #777; +} +#topo-p-instance .online svg text { + fill: #eee; +} + +#topo-p-instance svg text.instTitle { + font-size: 11pt; + font-weight: bold; +} +#topo-p-instance svg text.instLabel { + font-size: 9pt; + font-style: italic; +} + +#topo-p-instance .onosInst.mastership { + opacity: 0.3; +} +#topo-p-instance .onosInst.mastership.affinity { + opacity: 1.0; +} +#topo-p-instance .onosInst.mastership.affinity svg rect { + /* TODO: add blue glow */ + /*filter: url(#blue-glow);*/ +} 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 4a8b208aaf..c4a30b5ea7 100644 --- a/web/gui/src/main/webapp/app/view/topo/topo.js +++ b/web/gui/src/main/webapp/app/view/topo/topo.js @@ -143,12 +143,13 @@ .controller('OvTopoCtrl', [ '$scope', '$log', '$location', '$timeout', - 'FnService', 'MastService', - 'KeyService', 'ZoomService', 'GlyphService', 'MapService', + 'FnService', 'MastService', 'KeyService', 'ZoomService', + 'GlyphService', 'MapService', 'TopoEventService', 'TopoForceService', 'TopoPanelService', + 'TopoInstService', function ($scope, _$log_, $loc, $timeout, _fs_, mast, - _ks_, _zs_, _gs_, _ms_, tes, _tfs_, tps) { + _ks_, _zs_, _gs_, _ms_, tes, _tfs_, tps, tis) { var self = this; $log = _$log_; fs = _fs_; @@ -167,6 +168,7 @@ $log.log('OvTopoCtrl is saying Buh-Bye!'); tes.closeSock(); tps.destroyPanels(); + tis.destroyInst(); }); // svg layer and initialization of components @@ -181,6 +183,7 @@ setUpMap(); setUpForce(); + tis.initInst(); tps.initPanels(); tes.openSock(); diff --git a/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/web/gui/src/main/webapp/app/view/topo/topoEvent.js index 009c048124..fcff4e4f89 100644 --- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js +++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js @@ -23,7 +23,7 @@ 'use strict'; // injected refs - var $log, wss, wes, tps; + var $log, wss, wes, tps, tis; // internal state var wsock; @@ -47,7 +47,8 @@ } function addInstance(ev) { - $log.debug(' *** We got an ADD INSTANCE event: ', ev); + $log.debug(' **** Add Instance **** ', ev.payload); + tis.addInstance(ev.payload); } // ========================== @@ -87,13 +88,14 @@ angular.module('ovTopo') .factory('TopoEventService', ['$log', '$location', 'WebSocketService', 'WsEventService', - 'TopoPanelService', + 'TopoPanelService', 'TopoInstService', - function (_$log_, $loc, _wss_, _wes_, _tps_) { + function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_) { $log = _$log_; wss = _wss_; wes = _wes_; tps = _tps_; + tis = _tis_; function bindDispatcher(TopoDomElementsPassedHere) { // TODO: store refs to topo DOM elements... diff --git a/web/gui/src/main/webapp/app/view/topo/topoInst.js b/web/gui/src/main/webapp/app/view/topo/topoInst.js new file mode 100644 index 0000000000..0d3a2acbe3 --- /dev/null +++ b/web/gui/src/main/webapp/app/view/topo/topoInst.js @@ -0,0 +1,304 @@ +/* + * Copyright 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 -- Topology Instances Module. + Defines modeling of ONOS instances. + */ + +(function () { + 'use strict'; + + // injected refs + var $log, ps, sus, gs; + + // configuration + var instCfg = { + rectPad: 8, + nodeOx: 9, + nodeOy: 9, + nodeDim: 40, + birdOx: 19, + birdOy: 21, + birdDim: 21, + uiDy: 45, + titleDy: 30, + textYOff: 20, + textYSpc: 15 + }, + showLogicErrors = true, + idIns = 'topo-p-instance', + instOpts = { + edge: 'left', + width: 20 + }; + + // internal state + var onosInstances, + onosOrder, + oiShowMaster, + oiBox; + + + // ========================== + // *** ADD INSTANCE *** + + function addInstance(data) { + var id = data.id; + + if (onosInstances[id]) { + updateInstance(data); + return; + } + onosInstances[id] = data; + onosOrder.push(data); + updateInstances(); + } + + function updateInstance(data) { + var id = data.id, + d = onosInstances[id]; + if (d) { + angular.extend(d, data); + updateInstances(); + } else { + logicError('updateInstance: lookup fail: ID = "' + id + '"'); + } + } + + function computeDim(self) { + var css = window.getComputedStyle(self); + return { + w: sus.stripPx(css.width), + h: sus.stripPx(css.height) + }; + } + + function clickInst(d) { + var el = d3.select(this), + aff = el.classed('affinity'); + if (!aff) { + setAffinity(el, d); + } else { + cancelAffinity(); + } + } + + function setAffinity(el, d) { + d3.selectAll('.onosInst') + .classed('mastership', true) + .classed('affinity', false); + el.classed('affinity', true); + + // TODO: suppress the layers and highlight only specific nodes... + //suppressLayers(true); + //node.each(function (n) { + // if (n.master === d.id) { + // n.el.classed('suppressed', false); + // } + //}); + oiShowMaster = true; + } + + function cancelAffinity() { + d3.selectAll('.onosInst') + .classed('mastership affinity', false); + + // TODO: restore layer state + //restoreLayerState(); + oiShowMaster = false; + } + + function instRectAttr(dim) { + var pad = instCfg.rectPad; + return { + x: pad, + y: pad, + width: dim.w - pad*2, + height: dim.h - pad*2, + rx: 6 + }; + } + + function viewBox(dim) { + return '0 0 ' + dim.w + ' ' + dim.h; + } + + function attachUiBadge(svg) { + gs.addGlyph(svg, 'uiAttached', 30, true, [12, instCfg.uiDy]) + .classed('badgeIcon uiBadge', true); + } + + function instColor(id, online) { + // TODO: fix this.. + //return cat7.get(id, !online, network.view.getTheme()); + return 'blue'; + } + + // ============================== + + function updateInstances() { + var onoses = oiBox.el().selectAll('.onosInst') + .data(onosOrder, function (d) { return d.id; }), + instDim = {w:0,h:0}, + c = instCfg; + + function nSw(n) { + return '# Switches: ' + n; + } + + // operate on existing onos instances if necessary + onoses.each(function (d) { + var el = d3.select(this), + svg = el.select('svg'); + instDim = computeDim(this); + + // update online state + el.classed('online', d.online); + + // update ui-attached state + svg.select('use.uiBadge').remove(); + if (d.uiAttached) { + attachUiBadge(svg); + } + + function updAttr(id, value) { + svg.select('text.instLabel.'+id).text(value); + } + + updAttr('ip', d.ip); + updAttr('ns', nSw(d.switches)); + }); + + + // operate on new onos instances + var entering = onoses.enter() + .append('div') + .attr('class', 'onosInst') + .classed('online', function (d) { return d.online; }) + .on('click', clickInst); + + entering.each(function (d) { + var el = d3.select(this), + rectAttr, + svg; + instDim = computeDim(this); + rectAttr = instRectAttr(instDim); + + svg = el.append('svg').attr({ + width: instDim.w, + height: instDim.h, + viewBox: viewBox(instDim) + }); + + svg.append('rect').attr(rectAttr); + + gs.addGlyph(svg, 'bird', 28, true, [14, 14]) + .classed('badgeIcon', true); + + if (d.uiAttached) { + attachUiBadge(svg); + } + + var left = c.nodeOx + c.nodeDim, + len = rectAttr.width - left, + hlen = len / 2, + midline = hlen + left; + + // title + svg.append('text') + .attr({ + class: 'instTitle', + x: midline, + y: c.titleDy + }) + .text(d.id); + + // a couple of attributes + var ty = c.titleDy + c.textYOff; + + function addAttr(id, label) { + svg.append('text').attr({ + class: 'instLabel ' + id, + x: midline, + y: ty + }).text(label); + ty += c.textYSpc; + } + + addAttr('ip', d.ip); + addAttr('ns', nSw(d.switches)); + }); + + // operate on existing + new onoses here + // set the affinity colors... + onoses.each(function (d) { + var el = d3.select(this), + rect = el.select('svg').select('rect'), + col = instColor(d.id, d.online); + rect.style('fill', col); + }); + + // adjust the panel size appropriately... + oiBox.width(instDim.w * onosOrder.length); + oiBox.height(instDim.h); + + // remove any outgoing instances + onoses.exit().remove(); + } + + + // ========================== + + function logicError(msg) { + if (showLogicErrors) { + $log.warn('TopoInstService: ' + msg); + } + } + + function initInst() { + oiBox = ps.createPanel(idIns, instOpts); + oiBox.show(); + + onosInstances = {}; + onosOrder = []; + oiShowMaster = false; + } + + function destroyInst() { + ps.destroyPanel(idIns); + oiBox = null; + } + + // ========================== + + angular.module('ovTopo') + .factory('TopoInstService', + ['$log', 'PanelService', 'SvgUtilService', 'GlyphService', + + function (_$log_, _ps_, _sus_, _gs_) { + $log = _$log_; + ps = _ps_; + sus = _sus_; + gs = _gs_; + + return { + initInst: initInst, + destroyInst: destroyInst, + addInstance: addInstance + }; + }]); +}()); diff --git a/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/web/gui/src/main/webapp/app/view/topo/topoPanel.js index 51c0842a04..627a94e835 100644 --- a/web/gui/src/main/webapp/app/view/topo/topoPanel.js +++ b/web/gui/src/main/webapp/app/view/topo/topoPanel.js @@ -28,43 +28,20 @@ // constants var idSum = 'topo-p-summary', idDet = 'topo-p-detail', - idIns = 'topo-p-instance', panelOpts = { width: 260 }; - // internal state - var settings; - - - // SVG elements; - var fooPane; - - // D3 selections; + // panels var summaryPanel, - detailPanel, - instancePanel; - - // default settings for force layout - var defaultSettings = { - foo: 2 - }; - + detailPanel; // ========================== + // *** SHOW SUMMARY *** - function addSep(tbody) { - tbody.append('tr').append('td').attr('colspan', 2).append('hr'); - } - - function addProp(tbody, label, value) { - var tr = tbody.append('tr'); - - function addCell(cls, txt) { - tr.append('td').attr('class', cls).text(txt); - } - addCell('label', label + ' :'); - addCell('value', value); + function showSummary(data) { + populateSummary(data); + showSummaryPanel(); } function populateSummary(data) { @@ -89,9 +66,36 @@ }); } + function addSep(tbody) { + tbody.append('tr').append('td').attr('colspan', 2).append('hr'); + } + + function addProp(tbody, label, value) { + var tr = tbody.append('tr'); + + function addCell(cls, txt) { + tr.append('td').attr('class', cls).text(txt); + } + addCell('label', label + ' :'); + addCell('value', value); + } + function showSummaryPanel() { summaryPanel.show(); + // TODO: augment, once we have the details pane also + } + // ========================== + + function initPanels() { + summaryPanel = ps.createPanel(idSum, panelOpts); + detailPanel = ps.createPanel(idDet, panelOpts); + } + + function destroyPanels() { + ps.destroyPanel(idSum); + ps.destroyPanel(idDet); + summaryPanel = detailPanel = null; } // ========================== @@ -105,22 +109,6 @@ ps = _ps_; gs = _gs_; - function initPanels() { - summaryPanel = ps.createPanel(idSum, panelOpts); - // TODO: set up detail and instance panels.. - } - - function destroyPanels() { - ps.destroyPanel(idSum); - summaryPanel = null; - // TODO: destroy detail and instance panels.. - } - - function showSummary(payload) { - populateSummary(payload); - showSummaryPanel(); - } - return { initPanels: initPanels, destroyPanels: destroyPanels, 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 41a9ff921f..64c54f115b 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 @@ -84,6 +84,13 @@ describe('factory: fw/layer/panel.js', function () { expect(el.style('width')).toEqual('200px'); }); + it('should provide an api of panel functions', function () { + var p = ps.createPanel('foo'); + expect(fs.areFunctions(p, [ + 'show', 'hide', 'empty', 'append', 'width', 'height', 'isVisible', 'el' + ])).toBeTruthy(); + }); + it('should complain when a duplicate ID is used', function () { spyOn($log, 'warn'); var p = ps.createPanel('foo'); diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js index b72de5a066..a7179e3e41 100644 --- a/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js +++ b/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js @@ -252,7 +252,7 @@ describe('factory: fw/svg/glyph.js', function() { it('should add a glyph with default size', function () { gs.init(); - gs.addGlyph(svg, 'crown'); + var retval = gs.addGlyph(svg, 'crown'); var what = svg.selectAll('use'); expect(what.size()).toEqual(1); expect(what.attr('width')).toEqual('40'); @@ -260,6 +260,10 @@ describe('factory: fw/svg/glyph.js', function() { expect(what.attr('xlink:href')).toEqual('#crown'); expect(what.classed('glyph')).toBeTruthy(); expect(what.classed('overlay')).toBeFalsy(); + + // check a couple on retval, which should be the same thing.. + expect(retval.attr('xlink:href')).toEqual('#crown'); + expect(retval.classed('glyph')).toBeTruthy(); }); it('should add a glyph with given size', function () { diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js index 964320b611..c294e2a76d 100644 --- a/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js +++ b/web/gui/src/main/webapp/tests/app/fw/svg/svgUtil-spec.js @@ -43,7 +43,13 @@ describe('factory: fw/svg/svgUtil.js', function() { ])).toBeTruthy(); }); - // TODO: add unit tests for drag behavior etc. + + // TODO: add unit tests for drag behavior + // TODO: add unit tests for loadGlow + // TODO: add unit tests for cat7 + + + // === translate() it('should translate from two args', function () { expect(sus.translate(1,2)).toEqual('translate(1,2)'); @@ -53,4 +59,14 @@ describe('factory: fw/svg/svgUtil.js', function() { expect(sus.translate([3,4])).toEqual('translate(3,4)'); }); + + // === stripPx() + + it('should not affect a number', function () { + expect(sus.stripPx('4')).toEqual('4'); + }); + + it('should remove trailing px', function () { + expect(sus.stripPx('4px')).toEqual('4'); + }); }); diff --git a/web/gui/src/test/_karma/ev/migrate/ev_2_onos.json b/web/gui/src/test/_karma/ev/migrate/ev_2_onos.json new file mode 100644 index 0000000000..0579c1d5b8 --- /dev/null +++ b/web/gui/src/test/_karma/ev/migrate/ev_2_onos.json @@ -0,0 +1,14 @@ +{ + "event": "addInstance", + "payload": { + "id": "local", + "ip": "127.0.0.1", + "online": true, + "uiAttached": true, + "switches": 25, + "labels": [ + "local", + "127.0.0.1" + ] + } +}