Initial (v.rough) draft of ONOS UI.
Finally got something working, and need to check it in.
diff --git a/web/gui/src/main/webapp/base.css b/web/gui/src/main/webapp/base.css
new file mode 100644
index 0000000..51f16fe
--- /dev/null
+++ b/web/gui/src/main/webapp/base.css
@@ -0,0 +1,15 @@
+/*
+ Base CSS file
+
+ @author Simon Hunt
+ */
+
+html {
+ font-family: sans-serif;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+
+body {
+ margin: 0;
+}
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index d68a706..3b0d290 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -1,13 +1,54 @@
<!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>
diff --git a/web/gui/src/main/webapp/module-template.js b/web/gui/src/main/webapp/module-template.js
new file mode 100644
index 0000000..da049fe
--- /dev/null
+++ b/web/gui/src/main/webapp/module-template.js
@@ -0,0 +1,20 @@
+/*
+ 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));
diff --git a/web/gui/src/main/webapp/network.js b/web/gui/src/main/webapp/network.js
new file mode 100644
index 0000000..81105dc
--- /dev/null
+++ b/web/gui/src/main/webapp/network.js
@@ -0,0 +1,374 @@
+/*
+ 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));
+
diff --git a/web/gui/src/main/webapp/network.json b/web/gui/src/main/webapp/network.json
new file mode 100644
index 0000000..74a276a
--- /dev/null
+++ b/web/gui/src/main/webapp/network.json
@@ -0,0 +1,56 @@
+{
+ "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" }
+ ]
+}
diff --git a/web/gui/src/main/webapp/onos.css b/web/gui/src/main/webapp/onos.css
new file mode 100644
index 0000000..328e109
--- /dev/null
+++ b/web/gui/src/main/webapp/onos.css
@@ -0,0 +1,124 @@
+/*
+ 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;
+}
diff --git a/web/gui/src/main/webapp/onosui.js b/web/gui/src/main/webapp/onosui.js
new file mode 100644
index 0000000..3b62548
--- /dev/null
+++ b/web/gui/src/main/webapp/onosui.js
@@ -0,0 +1,79 @@
+/*
+ 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