GUI -- Test events: unpinned the first node; increased a few link widths.
- added alerts pane to framework.
- added library registration mechanism to framework.
- created d3Utils library
- reimplemented drag behavior of nodes.
Change-Id: I501f4ab6eded8393948cede903573580599258b1
diff --git a/web/gui/src/main/webapp/d3Utils.js b/web/gui/src/main/webapp/d3Utils.js
new file mode 100644
index 0000000..51651fa
--- /dev/null
+++ b/web/gui/src/main/webapp/d3Utils.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Utility functions for D3 visualizations.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+ 'use strict';
+
+ function createDragBehavior(force, selectCb, atDragEnd) {
+ var draggedThreshold = d3.scale.linear()
+ .domain([0, 0.1])
+ .range([5, 20])
+ .clamp(true),
+ drag;
+
+ // TODO: better validation of parameters
+ if (!$.isFunction(selectCb)) {
+ alert('d3util.createDragBehavior(): selectCb is not a function')
+ }
+ if (!$.isFunction(atDragEnd)) {
+ alert('d3util.createDragBehavior(): atDragEnd is not a function')
+ }
+
+ function dragged(d) {
+ var threshold = draggedThreshold(force.alpha()),
+ dx = d.oldX - d.px,
+ dy = d.oldY - d.py;
+ if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
+ d.dragged = true;
+ }
+ return d.dragged;
+ }
+
+ drag = d3.behavior.drag()
+ .origin(function(d) { return d; })
+ .on('dragstart', function(d) {
+ d.oldX = d.x;
+ d.oldY = d.y;
+ d.dragged = false;
+ d.fixed |= 2;
+ })
+ .on('drag', function(d) {
+ d.px = d3.event.x;
+ d.py = d3.event.y;
+ if (dragged(d)) {
+ if (!force.alpha()) {
+ force.alpha(.025);
+ }
+ }
+ })
+ .on('dragend', function(d) {
+ if (!dragged(d)) {
+ // consider this the same as a 'click' (selection of node)
+ selectCb(d, this); // TODO: set 'this' context instead of param
+ }
+ d.fixed &= ~6;
+
+ // hook at the end of a drag gesture
+ atDragEnd(d, this); // TODO: set 'this' context instead of param
+ });
+
+ return drag;
+ }
+
+ function appendGlow(svg) {
+ // TODO: parameterize color
+
+ var glow = svg.append('filter')
+ .attr('x', '-50%')
+ .attr('y', '-50%')
+ .attr('width', '200%')
+ .attr('height', '200%')
+ .attr('id', 'blue-glow');
+
+ glow.append('feColorMatrix')
+ .attr('type', 'matrix')
+ .attr('values', '0 0 0 0 0 ' +
+ '0 0 0 0 0 ' +
+ '0 0 0 0 .7 ' +
+ '0 0 0 1 0 ');
+
+ glow.append('feGaussianBlur')
+ .attr('stdDeviation', 3)
+ .attr('result', 'coloredBlur');
+
+ glow.append('feMerge').selectAll('feMergeNode')
+ .data(['coloredBlur', 'SourceGraphic'])
+ .enter().append('feMergeNode')
+ .attr('in', String);
+ }
+
+ // === register the functions as a library
+ onos.ui.addLib('d3util', {
+ createDragBehavior: createDragBehavior,
+ appendGlow: appendGlow
+ });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 1ddc318..235c1d7 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -64,6 +64,9 @@
<div id="overlays">
<!-- NOTE: overlays injected here, as needed -->
</div>
+ <div id="alerts">
+ <!-- NOTE: alert content injected here, as needed -->
+ </div>
</div>
<!-- Initialize the UI...-->
@@ -76,6 +79,9 @@
});
</script>
+ <!-- Library module files included here -->
+ <script src="d3Utils.js"></script>
+
<!-- Framework module files included here -->
<script src="mast2.js"></script>
diff --git a/web/gui/src/main/webapp/json/eventTest_11.json b/web/gui/src/main/webapp/json/eventTest_11.json
index e907444..3b361d5 100644
--- a/web/gui/src/main/webapp/json/eventTest_11.json
+++ b/web/gui/src/main/webapp/json/eventTest_11.json
@@ -10,8 +10,8 @@
"?"
],
"metaUi": {
- "x": 832,
- "y": 223
+ "Zx": 832,
+ "Zy": 223
}
}
}
diff --git a/web/gui/src/main/webapp/json/eventTest_15.json b/web/gui/src/main/webapp/json/eventTest_15.json
index 3622122..30ba9f3 100644
--- a/web/gui/src/main/webapp/json/eventTest_15.json
+++ b/web/gui/src/main/webapp/json/eventTest_15.json
@@ -10,8 +10,8 @@
"?"
],
"metaUi": {
- "x": 840,
- "y": 290
+ "Zx": 840,
+ "Zy": 290
}
}
}
diff --git a/web/gui/src/main/webapp/json/eventTest_17.json b/web/gui/src/main/webapp/json/eventTest_17.json
index e217456..82272a4 100644
--- a/web/gui/src/main/webapp/json/eventTest_17.json
+++ b/web/gui/src/main/webapp/json/eventTest_17.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff05",
"dstPort": "10",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "80 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_23.json b/web/gui/src/main/webapp/json/eventTest_23.json
index 54383bc..fff0f2b 100644
--- a/web/gui/src/main/webapp/json/eventTest_23.json
+++ b/web/gui/src/main/webapp/json/eventTest_23.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff05",
"dstPort": "30",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_24.json b/web/gui/src/main/webapp/json/eventTest_24.json
index 2287f6c..756b6c1 100644
--- a/web/gui/src/main/webapp/json/eventTest_24.json
+++ b/web/gui/src/main/webapp/json/eventTest_24.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "20",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_30.json b/web/gui/src/main/webapp/json/eventTest_30.json
index ae2d4c1..a617f45 100644
--- a/web/gui/src/main/webapp/json/eventTest_30.json
+++ b/web/gui/src/main/webapp/json/eventTest_30.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "30",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_34.json b/web/gui/src/main/webapp/json/eventTest_34.json
index 96015b5..fa5e3bc 100644
--- a/web/gui/src/main/webapp/json/eventTest_34.json
+++ b/web/gui/src/main/webapp/json/eventTest_34.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "10",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_35.json b/web/gui/src/main/webapp/json/eventTest_35.json
new file mode 100644
index 0000000..c579e59
--- /dev/null
+++ b/web/gui/src/main/webapp/json/eventTest_35.json
@@ -0,0 +1,14 @@
+{
+ "event": "addLink",
+ "payload": {
+ "src": "of:0000ffffffffff04",
+ "srcPort": "27",
+ "dst": "of:0000ffffffffff08",
+ "dstPort": "10",
+ "type": "optical",
+ "linkWidth": 2,
+ "props" : {
+ "BW": "30 G"
+ }
+ }
+}
diff --git a/web/gui/src/main/webapp/onos2.css b/web/gui/src/main/webapp/onos2.css
index 983f288..748cc97 100644
--- a/web/gui/src/main/webapp/onos2.css
+++ b/web/gui/src/main/webapp/onos2.css
@@ -32,6 +32,34 @@
display: block;
}
+div#alerts {
+ display: none;
+ position: absolute;
+ z-index: 2000;
+ opacity: 0.65;
+ background-color: #006;
+ color: white;
+ top: 80px;
+ left: 40px;
+ padding: 3px 6px;
+ box-shadow: 4px 6px 12px #777;
+}
+
+div#alerts pre {
+ margin: 0.2em 6px;
+}
+
+div#alerts span.close {
+ color: #6af;
+ float: right;
+ right: 2px;
+ cursor: pointer;
+}
+
+div#alerts span.close:hover {
+ color: #fff;
+}
+
/*
* ==============================================================
* END OF NEW ONOS.JS file
@@ -54,12 +82,6 @@
* Network Graph elements ======================================
*/
-svg .link {
- opacity: .7;
-}
-
-svg .link.host {
-}
svg g.portLayer rect.port {
fill: #ccc;
@@ -70,33 +92,13 @@
pointer-events: none;
}
-svg .node.device rect {
- stroke-width: 1.5px;
-}
-svg .node.device.fixed rect {
- stroke-width: 1.5;
- stroke: #ccc;
-}
-
-svg .node.device.roadm rect {
- fill: #03c;
-}
-
-svg .node.device.switch rect {
- fill: #06f;
-}
svg .node.host circle {
fill: #c96;
stroke: #000;
}
-svg .node text {
- fill: white;
- font: 10pt sans-serif;
- pointer-events: none;
-}
/* for debugging */
svg .node circle.debug {
@@ -110,10 +112,6 @@
}
-svg .node.selected rect,
-svg .node.selected circle {
- filter: url(#blue-glow);
-}
svg .link.inactive,
svg .port.inactive,
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 375fe6b..31d89fa 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -32,7 +32,8 @@
$.onos = function (options) {
var uiApi,
viewApi,
- navApi;
+ navApi,
+ libApi;
var defaultOptions = {
trace: false,
@@ -331,6 +332,58 @@
}
}
+ var alerts = {
+ open: false,
+ count: 0
+ };
+
+ function createAlerts() {
+ var al = d3.select('#alerts')
+ .style('display', 'block');
+ al.append('span')
+ .attr('class', 'close')
+ .text('X')
+ .on('click', closeAlerts);
+ al.append('pre');
+ alerts.open = true;
+ alerts.count = 0;
+ }
+
+ function closeAlerts() {
+ d3.select('#alerts')
+ .style('display', 'none');
+ d3.select('#alerts span').remove();
+ d3.select('#alerts pre').remove();
+ alerts.open = false;
+ }
+
+ function addAlert(msg) {
+ var lines,
+ oldContent;
+
+ if (alerts.count) {
+ oldContent = d3.select('#alerts pre').html();
+ }
+
+ lines = msg.split('\n');
+ lines[0] += ' '; // spacing so we don't crowd 'X'
+ lines = lines.join('\n');
+
+ if (oldContent) {
+ lines += '\n----\n' + oldContent;
+ }
+
+ d3.select('#alerts pre').html(lines);
+ alerts.count++;
+ }
+
+ function doAlert(msg) {
+ if (!alerts.open) {
+ createAlerts();
+ }
+ addAlert(msg);
+ }
+
function keyIn() {
var event = d3.event,
keyCode = event.keyCode,
@@ -408,7 +461,8 @@
uid: this.uid,
setRadio: this.setRadio,
setKeys: this.setKeys,
- dataLoadError: this.dataLoadError
+ dataLoadError: this.dataLoadError,
+ alert: this.alert
}
},
@@ -501,14 +555,20 @@
return makeUid(this, id);
},
- // TODO : implement custom dialogs (don't use alerts)
+ // TODO : implement custom dialogs
+
+ // Consider enhancing alert mechanism to handle multiples
+ // as individually closable.
+ alert: function (msg) {
+ doAlert(msg);
+ },
dataLoadError: function (err, url) {
var msg = 'Data Load Error\n\n' +
err.status + ' -- ' + err.statusText + '\n\n' +
'relative-url: "' + url + '"\n\n' +
'complete-url: "' + err.responseURL + '"';
- alert(msg);
+ this.alert(msg);
}
// TODO: consider schedule, clearTimer, etc.
@@ -521,6 +581,12 @@
// UI API
uiApi = {
+ addLib: function (libName, api) {
+ // TODO: validation of args
+ libApi[libName] = api;
+ },
+
+ // TODO: it remains to be seen whether we keep this style of docs
/** @api ui addView( vid, nid, cb )
* Adds a view to the UI.
* <p>
@@ -590,6 +656,12 @@
};
// ..........................................................
+ // Library API
+ libApi = {
+
+ };
+
+ // ..........................................................
// Exported API
// function to be called from index.html to build the ONOS UI
@@ -623,7 +695,8 @@
// export the api and build-UI function
return {
ui: uiApi,
- view: viewApi,
+ lib: libApi,
+ //view: viewApi,
nav: navApi,
buildUi: buildOnosUi
};
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 8293ce4..c4d9a2d 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -24,12 +24,18 @@
opacity: 0.5;
}
+/* NODES */
+
svg .node.device {
stroke: none;
stroke-width: 1.5px;
cursor: pointer;
}
+svg .node.device rect {
+ stroke-width: 1.5px;
+}
+
svg .node.device.fixed rect {
stroke-width: 1.5;
stroke: #ccc;
@@ -50,6 +56,17 @@
pointer-events: none;
}
+svg .node.selected rect,
+svg .node.selected circle {
+ filter: url(#blue-glow);
+}
+
+/* LINKS */
+
+svg .link {
+ opacity: .7;
+}
+
/* for debugging */
svg .node circle.debug {
fill: white;
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index bd5a577..d345fda 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -23,6 +23,9 @@
(function (onos) {
'use strict';
+ // shorter names for library APIs
+ var d3u = onos.lib.d3util;
+
// configuration data
var config = {
useLiveData: false,
@@ -61,6 +64,10 @@
height: 14
}
},
+ topo: {
+ linkInColor: '#66f',
+ linkInWidth: 14
+ },
icons: {
w: 28,
h: 28,
@@ -106,7 +113,9 @@
// key bindings
var keyDispatch = {
space: injectTestEvent, // TODO: remove (testing only)
- // M: testMe, // TODO: remove (testing only)
+ S: injectStartupEvents, // TODO: remove (testing only)
+ A: testAlert, // TODO: remove (testing only)
+ M: testMe, // TODO: remove (testing only)
B: toggleBg,
G: toggleLayout,
@@ -141,7 +150,8 @@
// For Debugging / Development
var eventPrefix = 'json/eventTest_',
- eventNumber = 0;
+ eventNumber = 0,
+ alertNumber = 0;
function note(label, msg) {
console.log('NOTE: ' + label + ': ' + msg);
@@ -155,22 +165,12 @@
// ==============================
// Key Callbacks
+ function testAlert(view) {
+ alertNumber++;
+ view.alert("Test me! -- " + alertNumber);
+ }
+
function testMe(view) {
- svg.append('line')
- .attr({
- x1: 100,
- y1: 100,
- x2: 500,
- y2: 400,
- stroke: '#2f3',
- 'stroke-width': 8
- })
- .transition()
- .duration(1200)
- .attr({
- stroke: '#666',
- 'stroke-width': 6
- });
}
function injectTestEvent(view) {
@@ -187,6 +187,13 @@
});
}
+ function injectStartupEvents(view) {
+ var lastStartupEvent = 32;
+ while (eventNumber < lastStartupEvent) {
+ injectTestEvent(view);
+ }
+ }
+
function toggleBg() {
var vis = bgImg.style('visibility');
bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
@@ -370,6 +377,11 @@
return lnk;
}
+ function linkWidth(w) {
+ // w is number of links between nodes. Scale appropriately.
+ return w * 1.2;
+ }
+
function updateLinks() {
link = linkG.selectAll('.link')
.data(network.links, function (d) { return d.id; });
@@ -387,12 +399,12 @@
y1: function (d) { return d.y1; },
x2: function (d) { return d.x2; },
y2: function (d) { return d.y2; },
- stroke: '#66f',
- 'stroke-width': 10
+ stroke: config.topo.linkInColor,
+ 'stroke-width': config.topo.linkInWidth
})
.transition().duration(1000)
.attr({
- 'stroke-width': function (d) { return d.width; },
+ 'stroke-width': function (d) { return linkWidth(d.width); },
stroke: '#666' // TODO: remove explicit stroke, rather...
});
@@ -461,6 +473,10 @@
return box;
}
+ function mkSvgClass(d) {
+ return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
+ }
+
function updateNodes() {
node = nodeG.selectAll('.node')
.data(network.nodes, function (d) { return d.id; });
@@ -473,11 +489,11 @@
.append('g')
.attr({
id: function (d) { return safeId(d.id); },
- class: function (d) { return d.svgClass; },
+ class: mkSvgClass,
transform: function (d) { return translate(d.x, d.y); },
opacity: 0
})
- //.call(network.drag)
+ .call(network.drag)
//.on('mouseover', function (d) {})
//.on('mouseover', function (d) {})
.transition()
@@ -578,6 +594,9 @@
svg = view.$div.append('svg');
setSize(svg, view);
+ // add blue glow filter to svg layer
+ d3u.appendGlow(svg);
+
// load the background image
bgImg = svg.append('svg:image')
.attr({
@@ -612,6 +631,20 @@
return fcfg.charge[d.class] || -200;
}
+ function selectCb(d, self) {
+ // TODO: selectObject(d, self);
+ }
+
+ function atDragEnd(d, self) {
+ // once we've finished moving, pin the node in position,
+ // if it is a device (not a host)
+ if (d.class === 'device') {
+ d.fixed = true;
+ d3.select(self).classed('fixed', true)
+ // TODO: send new [x,y] back to server, via websocket.
+ }
+ }
+
// set up the force layout
network.force = d3.layout.force()
.size(forceDim)
@@ -621,8 +654,9 @@
.linkDistance(ldist)
.linkStrength(lstrg)
.on('tick', tick);
- }
+ network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
+ }
function load(view, ctx) {
// cache the view token, so network topo functions can access it