Simon Hunt

Initial (v.rough) draft of ONOS UI.

Finally got something working, and need to check it in.
/*
Base CSS file
@author Simon Hunt
*/
html {
font-family: sans-serif;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body {
margin: 0;
}
<!DOCTYPE html>
<!--
ONOS UI - single page web app
@author Simon Hunt
-->
<html>
<head>
<title>ONOS GUI</title>
<script src="libs/d3.min.js"></script>
<script src="libs/jquery-2.1.1.min.js"></script>
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="onos.css">
<script src="onosui.js"></script>
</head>
<body>
<h1>ONOS GUI</h1>
Sort of...
<div id="frame">
<div id="mast">
<span class="title">
ONOS Web UI
</span>
<span class="right">
<span class="radio">[one]</span>
<span class="radio">[two]</span>
<span class="radio">[three]</span>
</span>
</div>
<div id="view"></div>
</div>
// Initialize the UI...
<script type="text/javascript">
var ONOS = $.onos({note: "config, if needed"});
</script>
// include module files
// + mast.js
// + nav.js
// + .... application views
// for now, we are just bootstrapping the network visualization
<script src="network.js" type="text/javascript"></script>
// finally, build the UI
<script type="text/javascript">
$(ONOS.buildUi);
</script>
</body>
</html>
\ No newline at end of file
</html>
......
/*
Module template file.
@author Simon Hunt
*/
(function (onos) {
'use strict';
var api = onos.api;
// == define your functions here.....
// == register views here, with links to lifecycle callbacks
// api.addView('view-id', {/* callbacks */});
}(ONOS));
/*
ONOS network topology viewer - PoC version 1.0
@author Simon Hunt
*/
(function (onos) {
'use strict';
var api = onos.api;
var config = {
jsonUrl: 'network.json',
mastHeight: 32,
force: {
linkDistance: 150,
linkStrength: 0.9,
charge: -400,
ticksWithoutCollisions: 50,
marginLR: 20,
marginTB: 20,
translate: function() {
return 'translate(' +
config.force.marginLR + ',' +
config.force.marginTB + ')';
}
},
labels: {
padLR: 3,
padTB: 2,
marginLR: 3,
marginTB: 2
},
constraints: {
ypos: {
pkt: 0.3,
opt: 0.7
}
}
},
view = {},
network = {},
selected = {},
highlighted = null;
function loadNetworkView() {
// Hey, here I am, calling something on the ONOS api:
api.printTime();
resize();
d3.json(config.jsonUrl, function (err, data) {
if (err) {
alert('Oops! Error reading JSON...\n\n' +
'URL: ' + jsonUrl + '\n\n' +
'Error: ' + err.message);
return;
}
console.log("here is the JSON data...");
console.log(data);
network.data = data;
drawNetwork();
});
$(document).on('click', '.select-object', function() {
// when any object of class "select-object" is clicked...
// TODO: get a reference to the object via lookup...
var obj = network.lookup[$(this).data('id')];
if (obj) {
selectObject(obj);
}
// stop propagation of event (I think) ...
return false;
});
$(window).on('resize', resize);
}
// ========================================================
function drawNetwork() {
$('#view').empty();
prepareNodesAndLinks();
createLayout();
console.log("\n\nHere is the augmented network object...");
console.warn(network);
}
function prepareNodesAndLinks() {
network.lookup = {};
network.nodes = [];
network.links = [];
var nw = network.forceWidth,
nh = network.forceHeight;
network.data.nodes.forEach(function(n) {
var ypc = yPosConstraintForNode(n),
ix = Math.random() * 0.8 * nw + 0.1 * nw,
iy = ypc * nh,
node = {
id: n.id,
type: n.type,
status: n.status,
x: ix,
y: iy,
constraint: {
weight: 0.7,
y: iy
}
};
network.lookup[n.id] = node;
network.nodes.push(node);
});
function yPosConstraintForNode(n) {
return config.constraints.ypos[n.type] || 0.5;
}
network.data.links.forEach(function(n) {
var src = network.lookup[n.src],
dst = network.lookup[n.dst],
id = src.id + "~" + dst.id;
var link = {
id: id,
source: src,
target: dst,
strength: config.force.linkStrength
};
network.links.push(link);
});
}
function createLayout() {
network.force = d3.layout.force()
.nodes(network.nodes)
.links(network.links)
.linkStrength(function(d) { return d.strength; })
.size([network.forceWidth, network.forceHeight])
.linkDistance(config.force.linkDistance)
.charge(config.force.charge)
.on('tick', tick);
network.svg = d3.select('#view').append('svg')
.attr('width', view.width)
.attr('height', view.height)
.append('g')
.attr('transform', config.force.translate());
// TODO: svg.append('defs')
// TODO: glow/blur stuff
// TODO: legend (and auto adjust on scroll)
network.link = network.svg.append('g').selectAll('.link')
.data(network.force.links(), function(d) {return d.id})
.enter().append('line')
.attr('class', 'link');
// TODO: drag behavior
// TODO: closest node deselect
// TODO: add drag, mouseover, mouseout behaviors
network.node = network.svg.selectAll('.node')
.data(network.force.nodes(), function(d) {return d.id})
.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) {
return translate(d.x, d.y);
})
// .call(network.drag)
.on('mouseover', function(d) {})
.on('mouseout', function(d) {});
// TODO: augment stroke and fill functions
network.nodeRect = network.node.append('rect')
// TODO: css for node rects
.attr('rx', 5)
.attr('ry', 5)
.attr('stroke', function(d) { return '#000'})
.attr('fill', function(d) { return '#ddf'})
.attr('width', 60)
.attr('height', 24);
network.node.each(function(d) {
var node = d3.select(this),
rect = node.select('rect');
var text = node.append('text')
.text(d.id)
.attr('dx', '1em')
.attr('dy', '2.1em');
});
// this function is scheduled to happen soon after the given thread ends
setTimeout(function() {
network.node.each(function(d) {
// for every node, recompute size, padding, etc. so text fits
var node = d3.select(this),
text = node.selectAll('text'),
bounds = {},
first = true;
// NOTE: probably unnecessary code if we only have one line.
});
network.numTicks = 0;
network.preventCollisions = false;
network.force.start();
for (var i = 0; i < config.ticksWithoutCollisions; i++) {
network.force.tick();
}
network.preventCollisions = true;
$('#view').css('visibility', 'visible');
});
}
function translate(x, y) {
return 'translate(' + x + ',' + y + ')';
}
function tick(e) {
network.numTicks++;
// adjust the y-coord of each node, based on y-pos constraints
// network.nodes.forEach(function (n) {
// var z = e.alpha * n.constraint.weight;
// if (!isNaN(n.constraint.y)) {
// n.y = (n.constraint.y * z + n.y * (1 - z));
// }
// });
network.link
.attr('x1', function(d) {
return d.source.x;
})
.attr('y1', function(d) {
return d.source.y;
})
.attr('x2', function(d) {
return d.target.x;
})
.attr('y2', function(d) {
return d.target.y;
});
network.node
.attr('transform', function(d) {
return translate(d.x, d.y);
});
}
// $('#docs-close').on('click', function() {
// deselectObject();
// return false;
// });
// $(document).on('click', '.select-object', function() {
// var obj = graph.data[$(this).data('name')];
// if (obj) {
// selectObject(obj);
// }
// return false;
// });
function selectObject(obj, el) {
var node;
if (el) {
node = d3.select(el);
} else {
network.node.each(function(d) {
if (d == obj) {
node = d3.select(el = this);
}
});
}
if (!node) return;
if (node.classed('selected')) {
deselectObject();
return;
}
deselectObject(false);
selected = {
obj : obj,
el : el
};
highlightObject(obj);
node.classed('selected', true);
// TODO animate incoming info pane
// resize(true);
// TODO: check bounds of selected node and scroll into view if needed
}
function deselectObject(doResize) {
// Review: logic of 'resize(...)' function.
if (doResize || typeof doResize == 'undefined') {
resize(false);
}
// deselect all nodes in the network...
network.node.classed('selected', false);
selected = {};
highlightObject(null);
}
function highlightObject(obj) {
if (obj) {
if (obj != highlighted) {
// TODO set or clear "inactive" class on nodes, based on criteria
network.node.classed('inactive', function(d) {
// return (obj !== d &&
// d.relation(obj.id));
return (obj !== d);
});
// TODO: same with links
network.link.classed('inactive', function(d) {
return (obj !== d.source && obj !== d.target);
});
}
highlighted = obj;
} else {
if (highlighted) {
// clear the inactive flag (no longer suppressed visually)
network.node.classed('inactive', false);
network.link.classed('inactive', false);
}
highlighted = null;
}
}
function resize(showDetails) {
console.log("resize() called...");
var $details = $('#details');
if (typeof showDetails == 'boolean') {
var showingDetails = showDetails;
// TODO: invoke $details.show() or $details.hide()...
// $details[showingDetails ? 'show' : 'hide']();
}
view.height = window.innerHeight - config.mastHeight;
view.width = window.innerWidth;
$('#view')
.css('height', view.height + 'px')
.css('width', view.width + 'px');
network.forceWidth = view.width - config.force.marginLR;
network.forceHeight = view.height - config.force.marginTB;
}
// ======================================================================
// register with the UI framework
api.addView('network', {
load: loadNetworkView
});
}(ONOS));
{
"id": "network-v1",
"meta": {
"__comment_1__": "This is sample data for developing the ONOS UI",
"foo": "bar",
"zoo": "goo"
},
"nodes": [
{
"id": "switch-1",
"type": "opt",
"status": "good"
},
{
"id": "switch-2",
"type": "opt",
"status": "good"
},
{
"id": "switch-3",
"type": "opt",
"status": "good"
},
{
"id": "switch-4",
"type": "opt",
"status": "good"
},
{
"id": "switch-11",
"type": "pkt",
"status": "good"
},
{
"id": "switch-12",
"type": "pkt",
"status": "good"
},
{
"id": "switch-13",
"type": "pkt",
"status": "good"
}
],
"links": [
{ "src": "switch-1", "dst": "switch-2" },
{ "src": "switch-1", "dst": "switch-3" },
{ "src": "switch-1", "dst": "switch-4" },
{ "src": "switch-2", "dst": "switch-3" },
{ "src": "switch-2", "dst": "switch-4" },
{ "src": "switch-3", "dst": "switch-4" },
{ "src": "switch-13", "dst": "switch-3" },
{ "src": "switch-12", "dst": "switch-2" },
{ "src": "switch-11", "dst": "switch-1" }
]
}
/*
ONOS CSS file
@author Simon Hunt
*/
body, html {
height: 100%;
}
/*
* Classes
*/
span.title {
color: red;
font-size: 16pt;
font-style: italic;
}
span.radio {
color: darkslateblue;
}
span.right {
float: right;
}
/*
* === DEBUGGING ======
*/
svg {
border: 1px dashed red;
}
/*
* Network Graph elements ======================================
*/
.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
opacity: .7;
/*marker-end: url(#end);*/
transition: opacity 250ms;
-webkit-transition: opacity 250ms;
-moz-transition: opacity 250ms;
}
marker#end {
fill: #666;
stroke: #666;
stroke-width: 1.5px;
}
.node rect {
stroke-width: 1.5px;
transition: opacity 250ms;
-webkit-transition: opacity 250ms;
-moz-transition: opacity 250ms;
}
.node text {
fill: #000;
font: 10px sans-serif;
pointer-events: none;
}
.node.selected rect {
filter: url(#blue-glow);
}
.link.inactive,
.node.inactive rect,
.node.inactive text {
opacity: .2;
}
.node.inactive.selected rect,
.node.inactive.selected text {
opacity: .6;
}
.legend {
position: fixed;
}
.legend .category rect {
stroke-width: 1px;
}
.legend .category text {
fill: #000;
font: 10px sans-serif;
pointer-events: none;
}
/*
* =============================================================
*/
/*
* Specific structural elements
*/
#frame {
width: 100%;
height: 100%;
background-color: #ffd;
}
#mast {
height: 32px;
background-color: #dda;
vertical-align: baseline;
}
#main {
background-color: #99b;
}
/*
ONOS UI Framework.
@author Simon Hunt
*/
(function ($) {
'use strict';
var tsI = new Date().getTime(), // initialize time stamp
tsB; // build time stamp
// attach our main function to the jQuery object
$.onos = function (options) {
// private namespaces
var publicApi; // public api
// internal state
var views = {},
currentView = null,
built = false;
// DOM elements etc.
var $mast;
// various functions..................
// throw an error
function throwError(msg) {
// todo: maybe add tracing later
throw new Error(msg);
}
// define all the public api functions...
publicApi = {
printTime: function () {
console.log("the time is " + new Date());
},
addView: function (vid, cb) {
views[vid] = {
vid: vid,
cb: cb
};
// TODO: proper registration of views
// for now, make the one (and only) view current..
currentView = views[vid];
}
};
// function to be called from index.html to build the ONOS UI
function buildOnosUi() {
tsB = new Date().getTime();
tsI = tsB - tsI; // initialization duration
console.log('ONOS UI initialized in ' + tsI + 'ms');
if (built) {
throwError("ONOS UI already built!");
}
built = true;
// TODO: invoke hash navigation
// --- report build errors ---
// for now, invoke the one and only load function:
currentView.cb.load();
}
// export the api and build-UI function
return {
api: publicApi,
buildUi: buildOnosUi
};
};
}(jQuery));
\ No newline at end of file