Simon Hunt

GUI -- augmented hash parsing to include flags (after '?'), which are passed int…

…o view callbacks as a boolean map.
 - moved event test files into sub directories
 - prepared topo2.js for scenario choice via hash context and 'local' (and 'debug') flag.
 - added 'simple' scenario: 2 switches, 1 link, and 2 hosts.
 - augmented topo event dispatch for yet-to-be-implemented event handlers.
 - implemented addHost() event handler.

Change-Id: I06b032684fd4d5f85262d13d58ad10edae23b3ed
Showing 56 changed files with 494 additions and 82 deletions
......@@ -90,6 +90,7 @@
<script src="sampleAlt2.js"></script>
<script src="sampleRadio.js"></script>
<script src="sampleKeys.js"></script>
<script src="sampleHash.js"></script>
<!-- Contributed (application) views injected here -->
<!-- TODO: replace with template marker and inject refs server-side -->
......
{
"comments": [
"This scenario steps through adding a host intent."
],
"title": "Host Intent Scenario",
"params": {
"lastAuto": 0
}
}
\ No newline at end of file
{
"event": "addDevice",
"payload": {
"id": "of:0000ffffffff0008",
"type": "switch",
"online": false,
"labels": [
"0000ffffffff0008",
"FF:FF:FF:FF:00:08",
"sw-8"
],
"metaUi": {
"x": 400,
"y": 280
}
}
}
{
"event": "addDevice",
"payload": {
"id": "of:0000ffffffff0003",
"type": "switch",
"online": false,
"labels": [
"0000ffffffff0003",
"FF:FF:FF:FF:00:03",
"sw-3"
],
"metaUi": {
"x": 800,
"y": 280
}
}
}
{
"event": "addLink",
"payload": {
"src": "of:0000ffffffff0003",
"srcPort": "21",
"dst": "of:0000ffffffff0008",
"dstPort": "20",
"type": "infra",
"linkWidth": 2,
"props" : {
"BW": "70 G"
}
}
}
{
"event": "addHost",
"payload": {
"id": "00:00:00:00:00:03/-1",
"cp": {
"device": "of:0000ffffffff0003",
"port": 1
},
"labels": [
"10.0.0.3",
"00:00:00:00:00:03"
],
"metaUi": {
}
}
}
{
"event": "addHost",
"payload": {
"id": "00:00:00:00:00:08/-1",
"cp": {
"device": "of:0000ffffffff0008",
"port": 1
},
"labels": [
"10.0.0.8",
"00:00:00:00:00:08"
],
"metaUi": {
}
}
}
{
"comments": [
"Add two devices and one link (auto), and two hosts."
],
"title": "Simple Startup Scenario",
"params": {
"lastAuto": 0
}
}
\ No newline at end of file
{
"comments": [
"This scenario steps through adding devices and links.",
"(Typical 'start-ip' of the view.)"
],
"title": "Startup Scenario",
"params": {
"lastAuto": 32
}
}
\ No newline at end of file
......@@ -52,6 +52,7 @@
current = {
view: null,
ctx: '',
flags: {},
theme: settings.theme
},
built = false,
......@@ -110,6 +111,7 @@
function doError(msg) {
errorCount++;
console.error(msg);
doAlert(msg);
}
function trace(msg) {
......@@ -140,7 +142,7 @@
t = parseHash(hash);
if (!t || !t.vid) {
doError('Unable to parse target hash: ' + hash);
doError('Unable to parse target hash: "' + hash + '"');
}
view = views[t.vid];
......@@ -160,33 +162,72 @@
function parseHash(s) {
// extract navigation coordinates from the supplied string
// "vid,ctx" --> { vid:vid, ctx:ctx }
// "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
traceFn('parseHash', s);
var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
// look for use of flags, first
var vidctx,
vid,
ctx,
flags,
flagMap,
m;
// RE that includes flags ('?flag1,flag2')
m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
if (m) {
return { vid: m[1], ctx: m[2] };
vidctx = m[1];
flags = m[2];
flagMap = {};
} else {
// no flags
m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
if (m) {
vidctx = m[1];
} else {
// bad hash
return null;
}
}
vidctx = vidctx.split(',');
vid = vidctx[0];
ctx = vidctx[1];
if (flags) {
flags.split(',').forEach(function (f) {
flagMap[f.trim()] = true;
});
}
m = /^[#]{0,1}(\S+)$/.exec(s);
return m ? { vid: m[1] } : null;
return {
vid: vid.trim(),
ctx: ctx ? ctx.trim() : '',
flags: flagMap
};
}
function makeHash(t, ctx) {
function makeHash(t, ctx, flags) {
traceFn('makeHash');
// make a hash string from the given navigation coordinates.
// make a hash string from the given navigation coordinates,
// and optional flags map.
// if t is not an object, then it is a vid
var h = t,
c = ctx || '';
c = ctx || '',
f = $.isPlainObject(flags) ? flags : null;
if ($.isPlainObject(t)) {
h = t.vid;
c = t.ctx || '';
f = t.flags || null;
}
if (c) {
h += ',' + c;
}
if (f) {
h += '?' + d3.map(f).keys().join(',');
}
trace('hash = "' + h + '"');
return h;
}
......@@ -244,6 +285,9 @@
// set the specified view as current, while invoking the
// appropriate life-cycle callbacks
// first, we'll start by closing the alerts pane, if open
closeAlerts();
// if there is a current view, and it is not the same as
// the incoming view, then unload it...
if (current.view && (current.view.vid !== view.vid)) {
......@@ -258,10 +302,11 @@
// cache new view and context
current.view = view;
current.ctx = t.ctx || '';
current.flags = t.flags || {};
// preload is called only once, after the view is in the DOM
if (!view.preloaded) {
view.preload(current.ctx);
view.preload(current.ctx, current.flags);
view.preloaded = true;
}
......@@ -269,7 +314,7 @@
view.reset();
// load the view
view.load(current.ctx);
view.load(current.ctx, current.flags);
}
// generate 'unique' id by prefixing view id
......@@ -454,7 +499,7 @@
d3.selectAll('.onosView').call(setViewDimensions);
// allow current view to react to resize event...
if (current.view) {
current.view.resize(current.ctx);
current.view.resize(current.ctx, current.flags);
}
}
......@@ -521,13 +566,13 @@
}
},
preload: function (ctx) {
preload: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.preload);
traceFn('View.preload', this.vid + ', ' + c);
if (fn) {
trace('PRELOAD cb for ' + this.vid);
fn(this.token(), c);
fn(this.token(), c, flags);
}
},
......@@ -544,15 +589,14 @@
}
},
load: function (ctx) {
load: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.load);
traceFn('View.load', this.vid + ', ' + c);
this.$div.classed('currentView', true);
// TODO: add radio button set, if needed
if (fn) {
trace('LOAD cb for ' + this.vid);
fn(this.token(), c);
fn(this.token(), c, flags);
}
},
......@@ -560,14 +604,13 @@
var fn = isF(this.cb.unload);
traceFn('View.unload', this.vid);
this.$div.classed('currentView', false);
// TODO: remove radio button set, if needed
if (fn) {
trace('UNLOAD cb for ' + this.vid);
fn(this.token());
}
},
resize: function (ctx) {
resize: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.resize),
w = this.width(),
......@@ -576,7 +619,7 @@
' [' + w + 'x' + h + ']');
if (fn) {
trace('RESIZE cb for ' + this.vid);
fn(this.token(), c);
fn(this.token(), c, flags);
}
},
......
......@@ -21,12 +21,17 @@
*/
(function () {
// NOTE: DON'T Want to do this.. we want to be able to
// use the parameter section, for example:
// #viewId,context?flag1,flag2,flag3
// Check if the URL in the address bar contains a parameter section
// (delineated by '?'). If this is the case, rewrite using '#' instead.
var m = /([^?]*)\?(.*)/.exec(window.location.href);
if (m) {
window.location.href = m[1] + '#' + m[2];
}
//var m = /([^?]*)\?(.*)/.exec(window.location.href);
//if (m) {
// window.location.href = m[1] + '#' + m[2];
//}
}());
......
/*
* 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.
*/
/*
Sample view to illustrate hash formats.
@author Simon Hunt
*/
(function (onos) {
'use strict';
var intro = "Try using the following hashes in the address bar:",
hashPrefix = '#sampleHash',
suffixes = [
'',
',one',
',two',
',context,ignored',
',context,ignored?a,b,c',
',two?foo',
',three?foo,bar'
],
$d;
function note(txt) {
$d.append('p')
.text(txt)
.style({
'font-size': '10pt',
color: 'darkorange',
padding: '0 20px',
margin: 0
});
}
function para(txt, color) {
var c = color || 'black';
$d.append('p')
.text(txt)
.style({
padding: '2px 8px',
color: c
});
}
function load(view, ctx, flags) {
var c = ctx || '(undefined)',
f = flags ? d3.map(flags).keys() : [];
$d = view.$div;
para(intro);
suffixes.forEach(function (s) {
note(hashPrefix + s);
});
para('View ID: ' + view.vid, 'blue');
para('Context: ' + c, 'blue');
para('Flags: { ' + f.join(', ') + ' }', 'magenta');
}
// == register the view here, with links to lifecycle callbacks
onos.ui.addView('sampleHash', {
reset: true, // empty the div on reset
load: load
});
}(ONOS));
......@@ -20,55 +20,59 @@
@author Simon Hunt
*/
svg #topo-bg {
#topo svg #topo-bg {
opacity: 0.5;
}
/* NODES */
svg .node.device {
#topo svg .node.device {
stroke: none;
stroke-width: 1.5px;
cursor: pointer;
}
svg .node.device rect {
#topo svg .node.device rect {
stroke-width: 1.5px;
}
svg .node.device.fixed rect {
#topo svg .node.device.fixed rect {
stroke-width: 1.5;
stroke: #ccc;
}
svg .node.device.switch {
#topo svg .node.device.switch {
fill: #17f;
}
svg .node.device.roadm {
#topo svg .node.device.roadm {
fill: #03c;
}
svg .node text {
#topo svg .node.host {
fill: #846;
}
#topo svg .node text {
stroke: none;
fill: white;
font: 10pt sans-serif;
pointer-events: none;
}
svg .node.selected rect,
svg .node.selected circle {
#topo svg .node.selected rect,
#topo svg .node.selected circle {
filter: url(#blue-glow);
}
/* LINKS */
svg .link {
#topo svg .link {
opacity: .7;
}
/* for debugging */
svg .node circle.debug {
#topo svg .node circle.debug {
fill: white;
stroke: red;
}
......
......@@ -132,8 +132,21 @@
links: [],
lookup: {}
},
scenario = {
evDir: 'json/ev/',
evScenario: '/scenario.json',
evPrefix: '/ev_',
evOnos: '_onos.json',
evUi: '_ui.json',
ctx: null,
params: {},
evNumber: 0,
view: null,
debug: false
},
webSock,
labelIdx = 0,
deviceLabelIndex = 0,
hostLabelIndex = 0,
selectOrder = [],
selections = {},
......@@ -155,10 +168,6 @@
// ==============================
// For Debugging / Development
var eventPrefix = 'json/eventTest_',
eventNumber = 0,
alertNumber = 0;
function note(label, msg) {
console.log('NOTE: ' + label + ': ' + msg);
}
......@@ -175,32 +184,71 @@
view.alert('test');
}
function injectTestEvent(view) {
function abortIfLive() {
if (config.useLiveData) {
view.alert("Sorry, currently using live data..");
return;
scenario.view.alert("Sorry, currently using live data..");
return true;
}
return false;
}
eventNumber++;
var eventUrl = eventPrefix + eventNumber + '.json';
function testDebug(msg) {
if (scenario.debug) {
scenario.view.alert(msg);
}
}
d3.json(eventUrl, function(err, data) {
function injectTestEvent(view) {
if (abortIfLive()) { return; }
var sc = scenario,
evn = ++sc.evNumber,
pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
onosUrl = pfx + sc.evOnos,
uiUrl = pfx + sc.evUi;
tryOnosEvent(onosUrl, uiUrl);
}
// TODO: tryOnosEvent/tryUiEvent folded into recursive function.
function tryOnosEvent(onosUrl, uiUrl) {
var v = scenario.view;
d3.json(onosUrl, function(err, data) {
if (err) {
view.dataLoadError(err, eventUrl);
if (err.status === 404) {
tryUiEvent(uiUrl);
} else {
v.alert('non-404 error:\n\n' + onosUrl + '\n\n' + err);
}
} else {
testDebug('loaded: ' + onosUrl);
handleServerEvent(data);
}
});
}
function tryUiEvent(uiUrl) {
var v = scenario.view;
d3.json(uiUrl, function(err, data) {
if (err) {
v.alert('Error:\n\n' + uiUrl + '\n\n' +
err.status + ': ' + err.statusText);
} else {
testDebug('loaded: ' + uiUrl);
handleUiEvent(data);
}
});
}
function handleUiEvent(data) {
testDebug('handleUiEvent(): ' + data.event);
// TODO:
}
function injectStartupEvents(view) {
if (config.useLiveData) {
view.alert("Sorry, currently using live data..");
return;
}
var last = scenario.params.lastAuto || 0;
if (abortIfLive()) { return; }
var lastStartupEvent = 32;
while (eventNumber < lastStartupEvent) {
while (scenario.evNumber < last) {
injectTestEvent(view);
}
}
......@@ -211,14 +259,16 @@
}
function cycleLabels() {
labelIdx = (labelIdx === network.deviceLabelCount - 1) ? 0 : labelIdx + 1;
deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
function niceLabel(label) {
return (label && label.trim()) ? label : '.';
}
network.nodes.forEach(function (d) {
var idx = (labelIdx < d.labels.length) ? labelIdx : 0,
if (d.class !== 'device') { return; }
var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
node = d3.select('#' + safeId(d.id)),
box;
......@@ -303,9 +353,14 @@
var eventDispatch = {
addDevice: addDevice,
updateDevice: updateDevice,
removeDevice: removeDevice,
updateDevice: stillToImplement,
removeDevice: stillToImplement,
addLink: addLink,
updateLink: stillToImplement,
removeLink: stillToImplement,
addHost: addHost,
updateHost: stillToImplement,
removeHost: stillToImplement,
showPath: showPath
};
......@@ -320,18 +375,6 @@
network.force.start();
}
function updateDevice(data) {
var device = data.payload;
note('updateDevice', device.id);
}
function removeDevice(data) {
var device = data.payload;
note('removeDevice', device.id);
}
function addLink(data) {
var link = data.payload,
lnk = createLink(link);
......@@ -345,11 +388,35 @@
}
}
function addHost(data) {
var host = data.payload,
node = createHostNode(host),
lnk;
note('addHost', node.id);
network.nodes.push(node);
network.lookup[host.id] = node;
updateNodes();
lnk = createHostLink(host);
if (lnk) {
network.links.push(lnk);
updateLinks();
}
network.force.start();
}
function showPath(data) {
network.view.alert(data.event + "\n" + data.payload.links.length);
}
// ....
// ...............................
function stillToImplement(data) {
var p = data.payload;
note(data.event, p.id);
network.view.alert('Not yet implemented: "' + data.event + '"');
}
function unknownEvent(data) {
network.view.alert('Unknown event type: "' + data.event + '"');
......@@ -367,6 +434,35 @@
return 'translate(' + x + ',' + y + ')';
}
function createHostLink(host) {
var src = host.id,
dst = host.cp.device,
srcNode = network.lookup[src],
dstNode = network.lookup[dst],
lnk;
if (!dstNode) {
// TODO: send warning message back to server on websocket
network.view.alert('switch not on map for link\n\n' +
'src = ' + src + '\ndst = ' + dst);
return null;
}
lnk = {
id: safeId(src) + '~' + safeId(dst),
source: srcNode,
target: dstNode,
class: 'link',
svgClass: 'link hostLink',
x1: srcNode.x,
y1: srcNode.y,
x2: dstNode.x,
y2: dstNode.y,
width: 1
};
return lnk;
}
function createLink(link) {
var type = link.type,
src = link.src,
......@@ -451,6 +547,22 @@
return node;
}
function createHostNode(host) {
// start with the object as is
var node = host;
// Augment as needed...
node.class = 'host';
node.svgClass = 'node host';
// TODO: consider placing near its switch, if [x,y] not defined
positionNode(node);
// cache label array length
network.hostLabelCount = host.labels.length;
return node;
}
function positionNode(node) {
var meta = node.metaUi,
x = 0,
......@@ -525,7 +637,7 @@
entering.filter('.device').each(function (d) {
var node = d3.select(this),
icon = iconUrl(d),
idx = (labelIdx < d.labels.length) ? labelIdx : 0,
idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
box;
node.append('rect')
......@@ -568,6 +680,32 @@
}
});
// augment host nodes...
entering.filter('.host').each(function (d) {
var node = d3.select(this),
idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0,
box;
node.append('circle')
.attr('r', 8); // TODO: define host circle radius
// TODO: are we attaching labels to hosts?
node.append('text')
.text(d.labels[idx])
.attr('dy', '1.1em');
// debug function to show the modelled x,y coordinates of nodes...
if (debug('showNodeXY')) {
node.select('circle').attr('fill-opacity', 0.5);
node.append('circle')
.attr({
class: 'debug',
cx: 0,
cy: 0,
r: '3px'
});
}
});
// operate on both existing and new nodes, if necessary
//node .foo() .bar() ...
......@@ -643,7 +781,7 @@
if (webSock.ws) {
webSock.ws.send(message);
} else {
network.view.alert('no web socket open');
network.view.alert('no web socket open\n\n' + message);
}
}
......@@ -714,7 +852,7 @@
//flyinPane(null);
}
// TODO: this click handler does not get unloaded when the view does
$('#view').on('click', function(e) {
if (!$(e.target).closest('.node').length) {
if (!e.metaKey) {
......@@ -723,6 +861,33 @@
}
});
function prepareScenario(view, ctx, dbg) {
var sc = scenario,
urlSc = sc.evDir + ctx + sc.evScenario;
if (!ctx) {
view.alert("No scenario specified (null ctx)");
return;
}
sc.view = view;
sc.ctx = ctx;
sc.debug = dbg;
sc.evNumber = 0;
d3.json(urlSc, function(err, data) {
var p = data && data.params || {};
if (err) {
view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
} else {
sc.params = p;
view.alert("Scenario loaded: " + ctx + '\n\n' + data.title);
}
});
}
// ==============================
// View life-cycle callbacks
......@@ -781,14 +946,11 @@
}
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);
if (config.useLiveData) {
tellServerCoords(d);
}
// once we've finished moving, pin the node in position
d.fixed = true;
d3.select(self).classed('fixed', true);
if (config.useLiveData) {
tellServerCoords(d);
}
}
......@@ -814,9 +976,14 @@
network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
}
function load(view, ctx) {
function load(view, ctx, flags) {
// cache the view token, so network topo functions can access it
network.view = view;
config.useLiveData = !flags.local;
if (!config.useLiveData) {
prepareScenario(view, ctx, flags.debug);
}
// set our radio buttons and key bindings
view.setRadio(btnSet);
......