GUI -- Added handling of hosts and links. (Still WIP).
Change-Id: I0ad3b16d47b264b6812f732f220230a2ae92de02
diff --git a/web/gui/src/main/webapp/app/fw/svg/icon.js b/web/gui/src/main/webapp/app/fw/svg/icon.js
index 21f9b9d..bd686f7 100644
--- a/web/gui/src/main/webapp/app/fw/svg/icon.js
+++ b/web/gui/src/main/webapp/app/fw/svg/icon.js
@@ -20,7 +20,7 @@
(function () {
'use strict';
- var $log, fs, gs;
+ var $log, fs, gs, sus;
var vboxSize = 50,
cornerSize = vboxSize / 10,
@@ -144,8 +144,21 @@
return g;
}
- function addHostIcon(elem, glyphId) {
- // TODO:
+ function addHostIcon(elem, radius, glyphId) {
+ var dim = radius * 1.5,
+ xlate = -dim / 2,
+ g = elem.append('g')
+ .attr('class', 'svgIcon hostIcon');
+
+ g.append('circle').attr('r', radius);
+
+ g.append('use').attr({
+ 'xlink:href': '#' + glyphId,
+ width: dim,
+ height: dim,
+ transform: sus.translate(xlate,xlate)
+ });
+ return g;
}
@@ -154,10 +167,13 @@
angular.module('onosSvg')
.factory('IconService', ['$log', 'FnService', 'GlyphService',
- function (_$log_, _fs_, _gs_) {
+ 'SvgUtilService',
+
+ function (_$log_, _fs_, _gs_, _sus_) {
$log = _$log_;
fs = _fs_;
gs = _gs_;
+ sus = _sus_;
return {
loadIcon: loadIcon,
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 ef61966..3ba7767 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -325,19 +325,121 @@
/* Host Nodes */
#ov-topo svg .node.host {
- stroke: #000;
}
#ov-topo svg .node.host text {
- fill: #846;
stroke: none;
font: 9pt sans-serif;
}
+.light #ov-topo svg .node.host text {
+ fill: #846;
+}
+.dark #ov-topo svg .node.host text {
+ fill: #BB809D;
+}
-svg .node.host circle {
+.light svg .node.host circle {
stroke: #000;
fill: #edb;
}
+.dark svg .node.host circle {
+ stroke: #eee;
+ fill: #B2A180;
+}
+.light svg .node.host .svgIcon {
+ fill: #444;
+}
+.dark svg .node.host .svgIcon {
+ fill: #222;
+}
+/* --- Topo Links --- */
+#ov-topo svg .link {
+ opacity: .9;
+}
+
+#ov-topo svg .link.inactive {
+ opacity: .5;
+ stroke-dasharray: 8 4;
+}
+
+#ov-topo svg .link.secondary {
+ stroke-width: 3px;
+}
+.light #ov-topo svg .link.secondary {
+ stroke: rgba(0,153,51,0.5);
+}
+.dark #ov-topo svg .link.secondary {
+ stroke: rgba(121,231,158,0.5);
+}
+
+#ov-topo svg .link.primary {
+ stroke-width: 4px;
+}
+.light #ov-topo svg .link.primary {
+ stroke: #ffA300;
+}
+.dark #ov-topo svg .link.primary {
+ stroke: #D58E0F;
+}
+
+.light #ov-topo svg .link.animated {
+ stroke: #ffA300;
+}
+.dark #ov-topo svg .link.animated {
+ stroke: #D58E0F;
+}
+
+#ov-topo svg .link.secondary.optical {
+ stroke-width: 4px;
+}
+.light #ov-topo svg .link.secondary.optical {
+ stroke: rgba(128,64,255,0.5);
+}
+.dark #ov-topo svg .link.secondary.optical {
+ stroke: rgba(164,139,215,0.5);
+}
+
+#ov-topo svg .link.primary.optical {
+ stroke-width: 6px;
+}
+.light #ov-topo svg .link.primary.optical {
+ stroke: #74f;
+}
+.dark #ov-topo svg .link.primary.optical {
+ stroke: #7352CD;
+}
+
+#ov-topo svg .link.animated.optical {
+ stroke-width: 10px;
+}
+.light #ov-topo svg .link.animated.optical {
+ stroke: #74f;
+}
+.dark #ov-topo svg .link.animated.optical {
+ stroke: #7352CD;
+}
+
+#ov-topo svg .linkLabel rect {
+ stroke: none;
+}
+.light #ov-topo svg .linkLabel rect {
+ fill: #eee;
+}
+.dark #ov-topo svg .linkLabel rect {
+ fill: #eee;
+}
+
+#ov-topo svg .linkLabel text {
+ text-anchor: middle;
+ stroke-width: 0.1;
+ font-size: 9pt;
+}
+.light #ov-topo svg .linkLabel text {
+ stroke: #777;
+}
+.dark #ov-topo svg .linkLabel text {
+ stroke: #777;
+}
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 7c30f87..4a2ae8a 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -198,12 +198,6 @@
return ms.loadMapInto(mapG, '*continental_us');
}
- // --- Force Layout --------------------------------------------------
-
- function setUpForce(xlink) {
- forceG = zoomLayer.append('g').attr('id', 'topo-force');
- tfs.initForce(forceG, xlink, svg.attr('width'), svg.attr('height'));
- }
// --- Controller Definition -----------------------------------------
@@ -219,8 +213,12 @@
function ($scope, _$log_, $loc, $timeout, _fs_, mast,
_ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) {
var self = this,
- xlink = {
- showNoDevs: showNoDevs
+ projection,
+ uplink = {
+ // provides function calls back into this space
+ showNoDevs: showNoDevs,
+ projection: function () { return projection; },
+ sendEvent: tes.sendEvent
};
$log = _$log_;
@@ -255,9 +253,15 @@
setUpDefs();
setUpZoom();
setUpNoDevs();
- xlink.projectionPromise = setUpMap();
- setUpForce(xlink);
+ setUpMap().then(
+ function (proj) {
+ projection = proj;
+ $log.debug('** We installed the projection: ', proj);
+ }
+ );
+ forceG = zoomLayer.append('g').attr('id', 'topo-force');
+ tfs.initForce(forceG, uplink, svg.attr('width'), svg.attr('height'));
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 6cd634f..e39e11c 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -34,9 +34,16 @@
updateInstance: updateInstance,
removeInstance: removeInstance,
addDevice: addDevice,
- updateDevice: updateDevice
- // TODO: implement remaining handlers..
+ updateDevice: updateDevice,
+ removeDevice: removeDevice,
+ addHost: addHost,
+ updateHost: updateHost,
+ removeHost: removeHost,
+ addLink: addLink,
+ updateLink: updateLink,
+ removeLink: removeLink
+ // TODO: implement remaining handlers..
};
function unknownEvent(ev) {
@@ -45,6 +52,9 @@
// === Event Handlers ===
+ // NOTE: --- once these are done, we will collapse them into
+ // a more compact data structure... but for now, write in full..
+
function showSummary(ev) {
$log.debug(' **** Show Summary **** ', ev.payload);
tps.showSummary(ev.payload);
@@ -75,6 +85,42 @@
tfs.updateDevice(ev.payload);
}
+ function removeDevice(ev) {
+ $log.debug(' **** Remove Device **** ', ev.payload);
+ tfs.removeDevice(ev.payload);
+ }
+
+ function addHost(ev) {
+ $log.debug(' **** Add Host **** ', ev.payload);
+ tfs.addHost(ev.payload);
+ }
+
+ function updateHost(ev) {
+ $log.debug(' **** Update Host **** ', ev.payload);
+ tfs.updateHost(ev.payload);
+ }
+
+ function removeHost(ev) {
+ $log.debug(' **** Remove Host **** ', ev.payload);
+ tfs.removeHost(ev.payload);
+ }
+
+ function addLink(ev) {
+ $log.debug(' **** Add Link **** ', ev.payload);
+ tfs.addLink(ev.payload);
+ }
+
+ function updateLink(ev) {
+ $log.debug(' **** Update Link **** ', ev.payload);
+ tfs.updateLink(ev.payload);
+ }
+
+ function removeLink(ev) {
+ $log.debug(' **** Remove Link **** ', ev.payload);
+ tfs.removeLink(ev.payload);
+ }
+
+
// ==========================
var dispatcher = {
@@ -122,14 +168,9 @@
tis = _tis_;
tfs = _tfs_;
- function bindDispatcher(TopoDomElementsPassedHere) {
- // TODO: store refs to topo DOM elements...
-
- return dispatcher;
- }
-
// TODO: handle "guiSuccessor" functionality (replace host)
// TODO: implement retry on close functionality
+
function openSock() {
wsock = wss.createWebSocket('topology', {
onOpen: onWsOpen,
@@ -151,9 +192,9 @@
}
return {
- bindDispatcher: bindDispatcher,
openSock: openSock,
- closeSock: closeSock
+ closeSock: closeSock,
+ sendEvent: dispatcher.sendEvent
};
}]);
}());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index 73cf9bf..01b719e 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -23,7 +23,9 @@
'use strict';
// injected refs
- var $log, sus, is, ts, tis, xlink;
+ var $log, fs, sus, is, ts, tis, uplink;
+
+ var icfg;
// configuration
var labelConfig = {
@@ -44,6 +46,21 @@
yoff: -18
};
+ var linkConfig = {
+ light: {
+ baseColor: '#666',
+ inColor: '#66f',
+ outColor: '#f00',
+ },
+ dark: {
+ baseColor: '#666',
+ inColor: '#66f',
+ outColor: '#f00',
+ },
+ inWidth: 12,
+ outWidth: 10
+ };
+
// internal state
var settings, // merged default settings and options
force, // force layout object
@@ -54,9 +71,11 @@
lookup: {},
revLinkToKey: {}
},
- projection, // background map projection
+ lu = network.lookup, // shorthand
deviceLabelIndex = 0, // for device label cycling
- hostLabelIndex = 0; // for host label cycling
+ hostLabelIndex = 0, // for host label cycling
+ showHosts = 1, // whether hosts are displayed
+ width, height;
// SVG elements;
var linkG, linkLabelG, nodeG;
@@ -99,18 +118,18 @@
var id = data.id,
d;
- xlink.showNoDevs(false);
+ uplink.showNoDevs(false);
// although this is an add device event, if we already have the
// device, treat it as an update instead..
- if (network.lookup[id]) {
+ if (lu[id]) {
updateDevice(data);
return;
}
d = createDeviceNode(data);
network.nodes.push(d);
- network.lookup[id] = d;
+ lu[id] = d;
$log.debug("Created new device.. ", d.id, d.x, d.y);
@@ -120,7 +139,7 @@
function updateDevice(data) {
var id = data.id,
- d = network.lookup[id],
+ d = lu[id],
wasOnline;
if (d) {
@@ -141,26 +160,379 @@
}
}
+ function removeDevice(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ removeDeviceElement(d);
+ } else {
+ // TODO: decide whether we want to capture logic errors
+ //logicError('removeDevice lookup fail. ID = "' + id + '"');
+ }
+ }
+
+ function addHost(data) {
+ var id = data.id,
+ d, lnk;
+
+ // although this is an add host event, if we already have the
+ // host, treat it as an update instead..
+ if (lu[id]) {
+ updateHost(data);
+ return;
+ }
+
+ d = createHostNode(data);
+ network.nodes.push(d);
+ lu[id] = d;
+
+ $log.debug("Created new host.. ", d.id, d.x, d.y);
+
+ updateNodes();
+
+ lnk = createHostLink(data);
+ if (lnk) {
+
+ $log.debug("Created new host-link.. ", lnk.key);
+
+ d.linkData = lnk; // cache ref on its host
+ network.links.push(lnk);
+ lu[d.ingress] = lnk;
+ lu[d.egress] = lnk;
+ updateLinks();
+ }
+
+ fStart();
+ }
+
+ function updateHost(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ angular.extend(d, data);
+ if (positionNode(d, true)) {
+ sendUpdateMeta(d, true);
+ }
+ updateNodes();
+ } else {
+ // TODO: decide whether we want to capture logic errors
+ //logicError('updateHost lookup fail. ID = "' + id + '"');
+ }
+ }
+
+ function removeHost(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ removeHostElement(d, true);
+ } else {
+ // may have already removed host, if attached to removed device
+ //console.warn('removeHost lookup fail. ID = "' + id + '"');
+ }
+ }
+
+ function addLink(data) {
+ var result = findLink(data, 'add'),
+ bad = result.badLogic,
+ d = result.ldata;
+
+ if (bad) {
+ //logicError(bad + ': ' + link.id);
+ return;
+ }
+
+ if (d) {
+ // we already have a backing store link for src/dst nodes
+ addLinkUpdate(d, data);
+ return;
+ }
+
+ // no backing store link yet
+ d = createLink(data);
+ if (d) {
+ network.links.push(d);
+ lu[d.key] = d;
+ updateLinks();
+ fStart();
+ }
+ }
+
+ function updateLink(data) {
+ var result = findLink(data, 'update'),
+ bad = result.badLogic;
+ if (bad) {
+ //logicError(bad + ': ' + link.id);
+ return;
+ }
+ result.updateWith(link);
+ }
+
+ function removeLink(data) {
+ var result = findLink(data, 'remove'),
+ bad = result.badLogic;
+ if (bad) {
+ // may have already removed link, if attached to removed device
+ //console.warn(bad + ': ' + link.id);
+ return;
+ }
+ result.removeRawLink();
+ }
+
+ // ========================
+
+ function addLinkUpdate(ldata, link) {
+ // add link event, but we already have the reverse link installed
+ ldata.fromTarget = link;
+ network.revLinkToKey[link.id] = ldata.key;
+ restyleLinkElement(ldata);
+ }
+
+ function createLink(link) {
+ var lnk = linkEndPoints(link.src, link.dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ angular.extend(lnk, {
+ key: link.id,
+ class: 'link',
+ fromSource: link,
+
+ // functions to aggregate dual link state
+ type: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget;
+ return (s && s.type) || (t && t.type) || defaultLinkType;
+ },
+ online: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ both = lnk.source.online && lnk.target.online;
+ return both && ((s && s.online) || (t && t.online));
+ },
+ linkWidth: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ ws = (s && s.linkWidth) || 0,
+ wt = (t && t.linkWidth) || 0;
+ return Math.max(ws, wt);
+ }
+ });
+ return lnk;
+ }
+
+
+ function makeNodeKey(d, what) {
+ var port = what + 'Port';
+ return d[what] + '/' + d[port];
+ }
+
+ function makeLinkKey(d, flipped) {
+ var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
+ two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
+ return one + '-' + two;
+ }
+
+ var widthRatio = 1.4,
+ linkScale = d3.scale.linear()
+ .domain([1, 12])
+ .range([widthRatio, 12 * widthRatio])
+ .clamp(true);
+
+ var allLinkTypes = 'direct indirect optical tunnel',
+ defaultLinkType = 'direct';
+
+ function restyleLinkElement(ldata) {
+ // this fn's job is to look at raw links and decide what svg classes
+ // need to be applied to the line element in the DOM
+ var th = ts.theme(),
+ el = ldata.el,
+ type = ldata.type(),
+ lw = ldata.linkWidth(),
+ online = ldata.online();
+
+ el.classed('link', true);
+ el.classed('inactive', !online);
+ el.classed(allLinkTypes, false);
+ if (type) {
+ el.classed(type, true);
+ }
+ el.transition()
+ .duration(1000)
+ .attr('stroke-width', linkScale(lw))
+ .attr('stroke', linkConfig[th].baseColor);
+ }
+
+ function findLink(linkData, op) {
+ var key = makeLinkKey(linkData),
+ keyrev = makeLinkKey(linkData, 1),
+ link = lu[key],
+ linkRev = lu[keyrev],
+ result = {},
+ ldata = link || linkRev,
+ rawLink;
+
+ if (op === 'add') {
+ if (link) {
+ // trying to add a link that we already know about
+ result.ldata = link;
+ result.badLogic = 'addLink: link already added';
+
+ } else if (linkRev) {
+ // we found the reverse of the link to be added
+ result.ldata = linkRev;
+ if (linkRev.fromTarget) {
+ result.badLogic = 'addLink: link already added';
+ }
+ }
+ } else if (op === 'update') {
+ if (!ldata) {
+ result.badLogic = 'updateLink: link not found';
+ } else {
+ rawLink = link ? ldata.fromSource : ldata.fromTarget;
+ result.updateWith = function (data) {
+ angular.extend(rawLink, data);
+ restyleLinkElement(ldata);
+ }
+ }
+ } else if (op === 'remove') {
+ if (!ldata) {
+ result.badLogic = 'removeLink: link not found';
+ } else {
+ rawLink = link ? ldata.fromSource : ldata.fromTarget;
+
+ if (!rawLink) {
+ result.badLogic = 'removeLink: link not found';
+
+ } else {
+ result.removeRawLink = function () {
+ if (link) {
+ // remove fromSource
+ ldata.fromSource = null;
+ if (ldata.fromTarget) {
+ // promote target into source position
+ ldata.fromSource = ldata.fromTarget;
+ ldata.fromTarget = null;
+ ldata.key = keyrev;
+ delete network.lookup[key];
+ network.lookup[keyrev] = ldata;
+ delete network.revLinkToKey[keyrev];
+ }
+ } else {
+ // remove fromTarget
+ ldata.fromTarget = null;
+ delete network.revLinkToKey[keyrev];
+ }
+ if (ldata.fromSource) {
+ restyleLinkElement(ldata);
+ } else {
+ removeLinkElement(ldata);
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+
+ function findAttachedHosts(devId) {
+ var hosts = [];
+ network.nodes.forEach(function (d) {
+ if (d.class === 'host' && d.cp.device === devId) {
+ hosts.push(d);
+ }
+ });
+ return hosts;
+ }
+
+ function findAttachedLinks(devId) {
+ var links = [];
+ network.links.forEach(function (d) {
+ if (d.source.id === devId || d.target.id === devId) {
+ links.push(d);
+ }
+ });
+ return links;
+ }
+
+ function removeLinkElement(d) {
+ var idx = fs.find(d.key, network.links, 'key'),
+ removed;
+ if (idx >=0) {
+ // remove from links array
+ removed = network.links.splice(idx, 1);
+ // remove from lookup cache
+ delete lu[removed[0].key];
+ updateLinks();
+ fResume();
+ }
+ }
+
+ function removeHostElement(d, upd) {
+ // first, remove associated hostLink...
+ removeLinkElement(d.linkData);
+
+ // remove hostLink bindings
+ delete lu[d.ingress];
+ delete lu[d.egress];
+
+ // remove from lookup cache
+ delete lu[d.id];
+ // remove from nodes array
+ var idx = fs.find(d.id, network.nodes);
+ network.nodes.splice(idx, 1);
+
+ // remove from SVG
+ // NOTE: upd is false if we were called from removeDeviceElement()
+ if (upd) {
+ updateNodes();
+ fResume();
+ }
+ }
+
+ function removeDeviceElement(d) {
+ var id = d.id;
+ // first, remove associated hosts and links..
+ findAttachedHosts(id).forEach(removeHostElement);
+ findAttachedLinks(id).forEach(removeLinkElement);
+
+ // remove from lookup cache
+ delete lu[id];
+ // remove from nodes array
+ var idx = fs.find(id, network.nodes);
+ network.nodes.splice(idx, 1);
+
+ if (!network.nodes.length) {
+ xlink.showNoDevs(true);
+ }
+
+ // remove from SVG
+ updateNodes();
+ fResume();
+ }
+
+
function sendUpdateMeta(d, store) {
var metaUi = {},
ll;
- // TODO: fix this code to send event to server...
- //if (store) {
- // ll = geoMapProj.invert([d.x, d.y]);
- // metaUi = {
- // x: d.x,
- // y: d.y,
- // lng: ll[0],
- // lat: ll[1]
- // };
- //}
- //d.metaUi = metaUi;
- //sendMessage('updateMeta', {
- // id: d.id,
- // 'class': d.class,
- // memento: metaUi
- //});
+ if (store) {
+ ll = lngLatFromCoord([d.x, d.y]);
+ metaUi = {
+ x: d.x,
+ y: d.y,
+ lng: ll[0],
+ lat: ll[1]
+ };
+ }
+ d.metaUi = metaUi;
+ uplink.sendEvent('updateMeta', {
+ id: d.id,
+ 'class': d.class,
+ memento: metaUi
+ });
}
@@ -178,9 +550,13 @@
// === Devices and hosts - helper functions
function coordFromLngLat(loc) {
- // Our hope is that the projection is installed before we start
- // handling incoming nodes. But if not, we'll just return the origin.
- return projection ? projection([loc.lng, loc.lat]) : [0, 0];
+ var p = uplink.projection();
+ return p ? p([loc.lng, loc.lat]) : [0, 0];
+ }
+
+ function lngLatFromCoord(coord) {
+ var p = uplink.projection();
+ return p ? p.invert([coord.x, coord.y]) : [0, 0];
}
function positionNode(node, forUpdate) {
@@ -230,8 +606,8 @@
function rand() {
return {
- x: randDim(network.view.width()),
- y: randDim(network.view.height())
+ x: randDim(width),
+ y: randDim(height)
};
}
@@ -246,7 +622,7 @@
}
function getDevice(cp) {
- var d = network.lookup[cp.device];
+ var d = lu[cp.device];
return d || rand();
}
@@ -267,9 +643,83 @@
return node;
}
+ function createHostNode(host) {
+ var node = host;
+
+ // Augment as needed...
+ node.class = 'host';
+ if (!node.type) {
+ node.type = 'endstation';
+ }
+ node.svgClass = 'node host ' + node.type;
+ positionNode(node);
+ return node;
+ }
+
+ function createHostLink(host) {
+ var src = host.id,
+ dst = host.cp.device,
+ id = host.ingress,
+ lnk = linkEndPoints(src, dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ // Synthesize link ...
+ angular.extend(lnk, {
+ key: id,
+ class: 'link',
+
+ type: function () { return 'hostLink'; },
+ online: function () {
+ // hostlink target is edge switch
+ return lnk.target.online;
+ },
+ linkWidth: function () { return 1; }
+ });
+ return lnk;
+ }
+
+ function linkEndPoints(srcId, dstId) {
+ var srcNode = lu[srcId],
+ dstNode = lu[dstId],
+ sMiss = !srcNode ? missMsg('src', srcId) : '',
+ dMiss = !dstNode ? missMsg('dst', dstId) : '';
+
+ if (sMiss || dMiss) {
+ $log.error('Node(s) not on map for link:\n' + sMiss + dMiss);
+ //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+ return null;
+ }
+ return {
+ source: srcNode,
+ target: dstNode,
+ x1: srcNode.x,
+ y1: srcNode.y,
+ x2: dstNode.x,
+ y2: dstNode.y
+ };
+ }
+
+ function missMsg(what, id) {
+ return '\n[' + what + '] "' + id + '" missing ';
+ }
+
// ==========================
// === Devices and hosts - D3 rendering
+ function nodeMouseOver(m) {
+ // TODO
+ $log.debug("TODO nodeMouseOver()...", m);
+ }
+
+ function nodeMouseOut(m) {
+ // TODO
+ $log.debug("TODO nodeMouseOut()...", m);
+ }
+
+
// Returns the newly computed bounding box of the rectangle
function adjustRectToFitText(n) {
var text = n.select('text'),
@@ -323,7 +773,7 @@
var label = trimLabel(deviceLabel(d)),
noLabel = !label,
node = d.el,
- dim = is.iconConfig().device.dim,
+ dim = icfg.device.dim,
devCfg = deviceIconConfig,
box, dx, dy;
@@ -357,16 +807,6 @@
d.el.select('text').text(label);
}
- function nodeMouseOver(m) {
- // TODO
- $log.debug("TODO nodeMouseOver()...", m);
- }
-
- function nodeMouseOut(m) {
- // TODO
- $log.debug("TODO nodeMouseOut()...", m);
- }
-
function updateDeviceColors(d) {
if (d) {
setDeviceColor(d);
@@ -445,13 +885,14 @@
return sus.cat7().getColor(id, !online, ts.theme());
}
- //============
+ // ==========================
function updateNodes() {
+ // select all the nodes in the layout:
node = nodeG.selectAll('.node')
.data(network.nodes, function (d) { return d.id; });
- // operate on existing nodes...
+ // operate on existing nodes:
node.filter('.device').each(deviceExisting);
node.filter('.host').each(hostExisting);
@@ -470,7 +911,7 @@
.transition()
.attr('opacity', 1);
- // augment nodes...
+ // augment entering nodes:
entering.filter('.device').each(deviceEnter);
entering.filter('.host').each(hostEnter);
@@ -486,7 +927,7 @@
.style('opacity', 0)
.remove();
- // node specific....
+ // exiting node specifics:
exiting.filter('.host').each(hostExit);
exiting.filter('.device').each(deviceExit);
@@ -539,25 +980,20 @@
}
function hostEnter(d) {
- var node = d3.select(this);
-
- //cfg = config.icons.host,
- //r = cfg.radius[d.type] || cfg.defaultRadius,
- //textDy = r + 10,
- //TODO: iid = iconGlyphUrl(d),
- // _dummy;
+ var node = d3.select(this),
+ gid = d.type || 'unknown',
+ rad = icfg.host.radius,
+ r = d.type ? rad.withGlyph : rad.noGlyph,
+ textDy = r + 10;
d.el = node;
+ sus.makeVisible(node, showHosts);
- //TODO: showHostVis(node);
+ is.addHostIcon(node, r, gid);
- node.append('circle').attr('r', r);
- //if (iid) {
- //TODO: addHostIcon(node, r, iid);
- //}
node.append('text')
.text(hostLabel)
- //.attr('dy', textDy)
+ .attr('dy', textDy)
.attr('text-anchor', 'middle');
}
@@ -598,6 +1034,160 @@
.style('opacity', 0.5);
}
+ // ==========================
+
+ function updateLinks() {
+ var th = ts.theme();
+
+ link = linkG.selectAll('.link')
+ .data(network.links, function (d) { return d.key; });
+
+ // operate on existing links:
+ //link.each(linkExisting);
+
+ // operate on entering links:
+ var entering = link.enter()
+ .append('line')
+ .attr({
+ x1: function (d) { return d.x1; },
+ y1: function (d) { return d.y1; },
+ x2: function (d) { return d.x2; },
+ y2: function (d) { return d.y2; },
+ stroke: linkConfig[th].inColor,
+ 'stroke-width': linkConfig.inWidth
+ });
+
+ // augment links
+ entering.each(linkEntering);
+
+ // operate on both existing and new links:
+ //link.each(...)
+
+ // apply or remove labels
+ var labelData = getLabelData();
+ applyLinkLabels(labelData);
+
+ // operate on exiting links:
+ link.exit()
+ .attr('stroke-dasharray', '3 3')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(1500)
+ .attr({
+ 'stroke-dasharray': '3 12',
+ stroke: linkConfig[th].outColor,
+ 'stroke-width': linkConfig.outWidth
+ })
+ .style('opacity', 0.0)
+ .remove();
+
+ // NOTE: invoke a single tick to force the labels to position
+ // onto their links.
+ tick();
+ // FIXME: this is a bug when in oblique view
+ // It causes the nodes to jump into "overhead" view positions, even
+ // though the oblique planes are still showing...
+ }
+
+ // ==========================
+ // updateLinks - subfunctions
+
+ function getLabelData() {
+ // create the backing data for showing labels..
+ var data = [];
+ link.each(function (d) {
+ if (d.label) {
+ data.push({
+ id: 'lab-' + d.key,
+ key: d.key,
+ label: d.label,
+ ldata: d
+ });
+ }
+ });
+ return data;
+ }
+
+ //function linkExisting(d) { }
+
+ function linkEntering(d) {
+ var link = d3.select(this);
+ d.el = link;
+ restyleLinkElement(d);
+ if (d.type() === 'hostLink') {
+ sus.makeVisible(link, showHosts);
+ }
+ }
+
+ //function linkExiting(d) { }
+
+ var linkLabelOffset = '0.3em';
+
+ function applyLinkLabels(data) {
+ var entering;
+
+ linkLabel = linkLabelG.selectAll('.linkLabel')
+ .data(data, function (d) { return d.id; });
+
+ // for elements already existing, we need to update the text
+ // and adjust the rectangle size to fit
+ linkLabel.each(function (d) {
+ var el = d3.select(this),
+ rect = el.select('rect'),
+ text = el.select('text');
+ text.text(d.label);
+ rect.attr(rectAroundText(el));
+ });
+
+ entering = linkLabel.enter().append('g')
+ .classed('linkLabel', true)
+ .attr('id', function (d) { return d.id; });
+
+ entering.each(function (d) {
+ var el = d3.select(this),
+ rect,
+ text,
+ parms = {
+ x1: d.ldata.x1,
+ y1: d.ldata.y1,
+ x2: d.ldata.x2,
+ y2: d.ldata.y2
+ };
+
+ d.el = el;
+ rect = el.append('rect');
+ text = el.append('text').text(d.label);
+ rect.attr(rectAroundText(el));
+ text.attr('dy', linkLabelOffset);
+
+ el.attr('transform', transformLabel(parms));
+ });
+
+ // Remove any labels that are no longer required.
+ linkLabel.exit().remove();
+ }
+
+ function rectAroundText(el) {
+ var text = el.select('text'),
+ box = text.node().getBBox();
+
+ // translate the bbox so that it is centered on [x,y]
+ box.x = -box.width / 2;
+ box.y = -box.height / 2;
+
+ // add padding
+ box.x -= 1;
+ box.width += 2;
+ return box;
+ }
+
+ function transformLabel(p) {
+ var dx = p.x2 - p.x1,
+ dy = p.y2 - p.y1,
+ xMid = dx/2 + p.x1,
+ yMid = dy/2 + p.y1;
+ return sus.translate(xMid, yMid);
+ }
// ==========================
// force layout tick function
@@ -620,34 +1210,31 @@
angular.module('ovTopo')
.factory('TopoForceService',
- ['$log', 'SvgUtilService', 'IconService', 'ThemeService',
+ ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
'TopoInstService',
- function (_$log_, _sus_, _is_, _ts_, _tis_) {
+ function (_$log_, _fs_, _sus_, _is_, _ts_, _tis_) {
$log = _$log_;
+ fs = _fs_;
sus = _sus_;
is = _is_;
ts = _ts_;
tis = _tis_;
+ icfg = is.iconConfig();
+
// forceG is the SVG group to display the force layout in
// xlink is the cross-link api from the main topo source file
// w, h are the initial dimensions of the SVG
// opts are, well, optional :)
- function initForce(forceG, _xlink_, w, h, opts) {
+ function initForce(forceG, _uplink_, w, h, opts) {
$log.debug('initForce().. WxH = ' + w + 'x' + h);
- xlink = _xlink_;
+ uplink = _uplink_;
+ width = w;
+ height = h;
settings = angular.extend({}, defaultSettings, opts);
- // when the projection promise is resolved, cache the projection
- xlink.projectionPromise.then(
- function (proj) {
- projection = proj;
- $log.debug('** We installed the projection: ', proj);
- }
- );
-
linkG = forceG.append('g').attr('id', 'topo-links');
linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
nodeG = forceG.append('g').attr('id', 'topo-nodes');
@@ -672,7 +1259,9 @@
}
function resize(dim) {
- force.size([dim.width, dim.height]);
+ width = dim.width;
+ height = dim.height;
+ force.size([width, height]);
// Review -- do we need to nudge the layout ?
}
@@ -683,7 +1272,14 @@
updateDeviceColors: updateDeviceColors,
addDevice: addDevice,
- updateDevice: updateDevice
+ updateDevice: updateDevice,
+ removeDevice: removeDevice,
+ addHost: addHost,
+ updateHost: updateHost,
+ removeHost: removeHost,
+ addLink: addLink,
+ updateLink: updateLink,
+ removeLink: removeLink
};
}]);
}());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoInst.js b/web/gui/src/main/webapp/app/view/topo/topoInst.js
index f68e46d..ee5c96c 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoInst.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoInst.js
@@ -323,9 +323,11 @@
return {
initInst: initInst,
destroyInst: destroyInst,
+
addInstance: addInstance,
updateInstance: updateInstance,
removeInstance: removeInstance,
+
isVisible: function () { return oiBox.isVisible(); },
show: function () { oiBox.show(); },
hide: function () { oiBox.hide(); }
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoEvent-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoEvent-spec.js
index 8cd6d25..b0a5be8 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoEvent-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoEvent-spec.js
@@ -34,7 +34,7 @@
it('should define api functions', function () {
expect(fs.areFunctions(tes, [
- 'bindDispatcher', 'openSock', 'closeSock'
+ 'openSock', 'closeSock', 'sendEvent'
])).toBeTruthy();
});
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
index cf4f4d6..b85272d 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
@@ -35,7 +35,9 @@
it('should define api functions', function () {
expect(fs.areFunctions(tfs, [
'initForce', 'resize', 'updateDeviceColors',
- 'addDevice', 'updateDevice'
+ 'addDevice', 'updateDevice', 'removeDevice',
+ 'addHost', 'updateHost', 'removeHost',
+ 'addLink', 'updateLink', 'removeLink'
])).toBeTruthy();
});
diff --git a/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json b/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json
index 993570b..35e4572 100644
--- a/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json
+++ b/web/gui/src/test/_karma/ev/simple/ev_8_addHost_03.json
@@ -12,6 +12,10 @@
"unknown",
"0E:2A:69:30:13:86"
],
+ "metaUi": {
+ "x": 800,
+ "y": 180
+ },
"props": {}
}
}
diff --git a/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json b/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json
index 17864a6..3d368c8 100644
--- a/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json
+++ b/web/gui/src/test/_karma/ev/simple/ev_9_addHost_08.json
@@ -12,6 +12,10 @@
"unknown",
"A6:96:E5:03:52:5F"
],
+ "metaUi": {
+ "x": 520,
+ "y": 250
+ },
"props": {}
}
}