Simon Hunt

GUI -- Implemented Instance Panel.

- handling addInstance event.

Change-Id: Ic98a3291bd37ecf1155dbe1696167d0635a31972
......@@ -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;
}
......
......@@ -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);
}
// ----------------------------------------------------------------------
......
......@@ -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
};
}]);
}());
......
......@@ -78,6 +78,7 @@
<script src="view/topo/topoEvent.js"></script>
<script src="view/topo/topoForce.js"></script>
<script src="view/topo/topoPanel.js"></script>
<script src="view/topo/topoInst.js"></script>
<script src="view/device/device.js"></script>
<!-- TODO: inject javascript refs server-side -->
......
......@@ -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);*/
}
......
......@@ -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();
......
......@@ -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...
......
/*
* 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
};
}]);
}());
......@@ -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,
......
......@@ -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');
......
......@@ -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 () {
......
......@@ -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');
});
});
......
{
"event": "addInstance",
"payload": {
"id": "local",
"ip": "127.0.0.1",
"online": true,
"uiAttached": true,
"switches": 25,
"labels": [
"local",
"127.0.0.1"
]
}
}