Simon Hunt

Initial (v.rough) draft of ONOS UI.

Finally got something working, and need to check it in.
1 +/*
2 + Base CSS file
3 +
4 + @author Simon Hunt
5 + */
6 +
7 +html {
8 + font-family: sans-serif;
9 + -webkit-text-size-adjust: 100%;
10 + -ms-text-size-adjust: 100%;
11 +}
12 +
13 +body {
14 + margin: 0;
15 +}
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 +<!--
3 + ONOS UI - single page web app
4 +
5 + @author Simon Hunt
6 + -->
2 <html> 7 <html>
3 <head> 8 <head>
4 <title>ONOS GUI</title> 9 <title>ONOS GUI</title>
5 10
6 <script src="libs/d3.min.js"></script> 11 <script src="libs/d3.min.js"></script>
7 <script src="libs/jquery-2.1.1.min.js"></script> 12 <script src="libs/jquery-2.1.1.min.js"></script>
13 +
14 + <link rel="stylesheet" href="base.css">
15 + <link rel="stylesheet" href="onos.css">
16 +
17 + <script src="onosui.js"></script>
18 +
8 </head> 19 </head>
9 <body> 20 <body>
10 - <h1>ONOS GUI</h1> 21 + <div id="frame">
11 - Sort of... 22 + <div id="mast">
23 + <span class="title">
24 + ONOS Web UI
25 + </span>
26 + <span class="right">
27 + <span class="radio">[one]</span>
28 + <span class="radio">[two]</span>
29 + <span class="radio">[three]</span>
30 + </span>
31 + </div>
32 + <div id="view"></div>
33 + </div>
34 +
35 + // Initialize the UI...
36 + <script type="text/javascript">
37 + var ONOS = $.onos({note: "config, if needed"});
38 + </script>
39 +
40 + // include module files
41 + // + mast.js
42 + // + nav.js
43 + // + .... application views
44 +
45 + // for now, we are just bootstrapping the network visualization
46 + <script src="network.js" type="text/javascript"></script>
47 +
48 + // finally, build the UI
49 + <script type="text/javascript">
50 + $(ONOS.buildUi);
51 + </script>
52 +
12 </body> 53 </body>
13 </html> 54 </html>
......
1 +/*
2 + Module template file.
3 +
4 + @author Simon Hunt
5 + */
6 +
7 +(function (onos) {
8 + 'use strict';
9 +
10 + var api = onos.api;
11 +
12 + // == define your functions here.....
13 +
14 +
15 + // == register views here, with links to lifecycle callbacks
16 +
17 +// api.addView('view-id', {/* callbacks */});
18 +
19 +
20 +}(ONOS));
1 +/*
2 + ONOS network topology viewer - PoC version 1.0
3 +
4 + @author Simon Hunt
5 + */
6 +
7 +(function (onos) {
8 + 'use strict';
9 +
10 + var api = onos.api;
11 +
12 + var config = {
13 + jsonUrl: 'network.json',
14 + mastHeight: 32,
15 + force: {
16 + linkDistance: 150,
17 + linkStrength: 0.9,
18 + charge: -400,
19 + ticksWithoutCollisions: 50,
20 + marginLR: 20,
21 + marginTB: 20,
22 + translate: function() {
23 + return 'translate(' +
24 + config.force.marginLR + ',' +
25 + config.force.marginTB + ')';
26 + }
27 + },
28 + labels: {
29 + padLR: 3,
30 + padTB: 2,
31 + marginLR: 3,
32 + marginTB: 2
33 + },
34 + constraints: {
35 + ypos: {
36 + pkt: 0.3,
37 + opt: 0.7
38 + }
39 + }
40 + },
41 + view = {},
42 + network = {},
43 + selected = {},
44 + highlighted = null;
45 +
46 +
47 + function loadNetworkView() {
48 + // Hey, here I am, calling something on the ONOS api:
49 + api.printTime();
50 +
51 + resize();
52 +
53 + d3.json(config.jsonUrl, function (err, data) {
54 + if (err) {
55 + alert('Oops! Error reading JSON...\n\n' +
56 + 'URL: ' + jsonUrl + '\n\n' +
57 + 'Error: ' + err.message);
58 + return;
59 + }
60 + console.log("here is the JSON data...");
61 + console.log(data);
62 +
63 + network.data = data;
64 + drawNetwork();
65 + });
66 +
67 + $(document).on('click', '.select-object', function() {
68 + // when any object of class "select-object" is clicked...
69 + // TODO: get a reference to the object via lookup...
70 + var obj = network.lookup[$(this).data('id')];
71 + if (obj) {
72 + selectObject(obj);
73 + }
74 + // stop propagation of event (I think) ...
75 + return false;
76 + });
77 +
78 + $(window).on('resize', resize);
79 + }
80 +
81 +
82 + // ========================================================
83 +
84 + function drawNetwork() {
85 + $('#view').empty();
86 +
87 + prepareNodesAndLinks();
88 + createLayout();
89 + console.log("\n\nHere is the augmented network object...");
90 + console.warn(network);
91 + }
92 +
93 + function prepareNodesAndLinks() {
94 + network.lookup = {};
95 + network.nodes = [];
96 + network.links = [];
97 +
98 + var nw = network.forceWidth,
99 + nh = network.forceHeight;
100 +
101 + network.data.nodes.forEach(function(n) {
102 + var ypc = yPosConstraintForNode(n),
103 + ix = Math.random() * 0.8 * nw + 0.1 * nw,
104 + iy = ypc * nh,
105 + node = {
106 + id: n.id,
107 + type: n.type,
108 + status: n.status,
109 + x: ix,
110 + y: iy,
111 + constraint: {
112 + weight: 0.7,
113 + y: iy
114 + }
115 + };
116 + network.lookup[n.id] = node;
117 + network.nodes.push(node);
118 + });
119 +
120 + function yPosConstraintForNode(n) {
121 + return config.constraints.ypos[n.type] || 0.5;
122 + }
123 +
124 +
125 + network.data.links.forEach(function(n) {
126 + var src = network.lookup[n.src],
127 + dst = network.lookup[n.dst],
128 + id = src.id + "~" + dst.id;
129 +
130 + var link = {
131 + id: id,
132 + source: src,
133 + target: dst,
134 + strength: config.force.linkStrength
135 + };
136 + network.links.push(link);
137 + });
138 + }
139 +
140 + function createLayout() {
141 +
142 + network.force = d3.layout.force()
143 + .nodes(network.nodes)
144 + .links(network.links)
145 + .linkStrength(function(d) { return d.strength; })
146 + .size([network.forceWidth, network.forceHeight])
147 + .linkDistance(config.force.linkDistance)
148 + .charge(config.force.charge)
149 + .on('tick', tick);
150 +
151 + network.svg = d3.select('#view').append('svg')
152 + .attr('width', view.width)
153 + .attr('height', view.height)
154 + .append('g')
155 + .attr('transform', config.force.translate());
156 +
157 + // TODO: svg.append('defs')
158 + // TODO: glow/blur stuff
159 + // TODO: legend (and auto adjust on scroll)
160 +
161 + network.link = network.svg.append('g').selectAll('.link')
162 + .data(network.force.links(), function(d) {return d.id})
163 + .enter().append('line')
164 + .attr('class', 'link');
165 +
166 + // TODO: drag behavior
167 + // TODO: closest node deselect
168 +
169 + // TODO: add drag, mouseover, mouseout behaviors
170 + network.node = network.svg.selectAll('.node')
171 + .data(network.force.nodes(), function(d) {return d.id})
172 + .enter().append('g')
173 + .attr('class', 'node')
174 + .attr('transform', function(d) {
175 + return translate(d.x, d.y);
176 + })
177 + // .call(network.drag)
178 + .on('mouseover', function(d) {})
179 + .on('mouseout', function(d) {});
180 +
181 + // TODO: augment stroke and fill functions
182 + network.nodeRect = network.node.append('rect')
183 + // TODO: css for node rects
184 + .attr('rx', 5)
185 + .attr('ry', 5)
186 + .attr('stroke', function(d) { return '#000'})
187 + .attr('fill', function(d) { return '#ddf'})
188 + .attr('width', 60)
189 + .attr('height', 24);
190 +
191 + network.node.each(function(d) {
192 + var node = d3.select(this),
193 + rect = node.select('rect');
194 + var text = node.append('text')
195 + .text(d.id)
196 + .attr('dx', '1em')
197 + .attr('dy', '2.1em');
198 + });
199 +
200 + // this function is scheduled to happen soon after the given thread ends
201 + setTimeout(function() {
202 + network.node.each(function(d) {
203 + // for every node, recompute size, padding, etc. so text fits
204 + var node = d3.select(this),
205 + text = node.selectAll('text'),
206 + bounds = {},
207 + first = true;
208 +
209 + // NOTE: probably unnecessary code if we only have one line.
210 + });
211 +
212 + network.numTicks = 0;
213 + network.preventCollisions = false;
214 + network.force.start();
215 + for (var i = 0; i < config.ticksWithoutCollisions; i++) {
216 + network.force.tick();
217 + }
218 + network.preventCollisions = true;
219 + $('#view').css('visibility', 'visible');
220 + });
221 +
222 + }
223 +
224 + function translate(x, y) {
225 + return 'translate(' + x + ',' + y + ')';
226 + }
227 +
228 +
229 + function tick(e) {
230 + network.numTicks++;
231 +
232 + // adjust the y-coord of each node, based on y-pos constraints
233 +// network.nodes.forEach(function (n) {
234 +// var z = e.alpha * n.constraint.weight;
235 +// if (!isNaN(n.constraint.y)) {
236 +// n.y = (n.constraint.y * z + n.y * (1 - z));
237 +// }
238 +// });
239 +
240 + network.link
241 + .attr('x1', function(d) {
242 + return d.source.x;
243 + })
244 + .attr('y1', function(d) {
245 + return d.source.y;
246 + })
247 + .attr('x2', function(d) {
248 + return d.target.x;
249 + })
250 + .attr('y2', function(d) {
251 + return d.target.y;
252 + });
253 +
254 + network.node
255 + .attr('transform', function(d) {
256 + return translate(d.x, d.y);
257 + });
258 +
259 + }
260 +
261 + // $('#docs-close').on('click', function() {
262 + // deselectObject();
263 + // return false;
264 + // });
265 +
266 + // $(document).on('click', '.select-object', function() {
267 + // var obj = graph.data[$(this).data('name')];
268 + // if (obj) {
269 + // selectObject(obj);
270 + // }
271 + // return false;
272 + // });
273 +
274 + function selectObject(obj, el) {
275 + var node;
276 + if (el) {
277 + node = d3.select(el);
278 + } else {
279 + network.node.each(function(d) {
280 + if (d == obj) {
281 + node = d3.select(el = this);
282 + }
283 + });
284 + }
285 + if (!node) return;
286 +
287 + if (node.classed('selected')) {
288 + deselectObject();
289 + return;
290 + }
291 + deselectObject(false);
292 +
293 + selected = {
294 + obj : obj,
295 + el : el
296 + };
297 +
298 + highlightObject(obj);
299 +
300 + node.classed('selected', true);
301 +
302 + // TODO animate incoming info pane
303 + // resize(true);
304 + // TODO: check bounds of selected node and scroll into view if needed
305 + }
306 +
307 + function deselectObject(doResize) {
308 + // Review: logic of 'resize(...)' function.
309 + if (doResize || typeof doResize == 'undefined') {
310 + resize(false);
311 + }
312 + // deselect all nodes in the network...
313 + network.node.classed('selected', false);
314 + selected = {};
315 + highlightObject(null);
316 + }
317 +
318 + function highlightObject(obj) {
319 + if (obj) {
320 + if (obj != highlighted) {
321 + // TODO set or clear "inactive" class on nodes, based on criteria
322 + network.node.classed('inactive', function(d) {
323 + // return (obj !== d &&
324 + // d.relation(obj.id));
325 + return (obj !== d);
326 + });
327 + // TODO: same with links
328 + network.link.classed('inactive', function(d) {
329 + return (obj !== d.source && obj !== d.target);
330 + });
331 + }
332 + highlighted = obj;
333 + } else {
334 + if (highlighted) {
335 + // clear the inactive flag (no longer suppressed visually)
336 + network.node.classed('inactive', false);
337 + network.link.classed('inactive', false);
338 + }
339 + highlighted = null;
340 +
341 + }
342 + }
343 +
344 + function resize(showDetails) {
345 + console.log("resize() called...");
346 +
347 + var $details = $('#details');
348 +
349 + if (typeof showDetails == 'boolean') {
350 + var showingDetails = showDetails;
351 + // TODO: invoke $details.show() or $details.hide()...
352 + // $details[showingDetails ? 'show' : 'hide']();
353 + }
354 +
355 + view.height = window.innerHeight - config.mastHeight;
356 + view.width = window.innerWidth;
357 + $('#view')
358 + .css('height', view.height + 'px')
359 + .css('width', view.width + 'px');
360 +
361 + network.forceWidth = view.width - config.force.marginLR;
362 + network.forceHeight = view.height - config.force.marginTB;
363 + }
364 +
365 + // ======================================================================
366 + // register with the UI framework
367 +
368 + api.addView('network', {
369 + load: loadNetworkView
370 + });
371 +
372 +
373 +}(ONOS));
374 +
1 +{
2 + "id": "network-v1",
3 + "meta": {
4 + "__comment_1__": "This is sample data for developing the ONOS UI",
5 + "foo": "bar",
6 + "zoo": "goo"
7 + },
8 + "nodes": [
9 + {
10 + "id": "switch-1",
11 + "type": "opt",
12 + "status": "good"
13 + },
14 + {
15 + "id": "switch-2",
16 + "type": "opt",
17 + "status": "good"
18 + },
19 + {
20 + "id": "switch-3",
21 + "type": "opt",
22 + "status": "good"
23 + },
24 + {
25 + "id": "switch-4",
26 + "type": "opt",
27 + "status": "good"
28 + },
29 + {
30 + "id": "switch-11",
31 + "type": "pkt",
32 + "status": "good"
33 + },
34 + {
35 + "id": "switch-12",
36 + "type": "pkt",
37 + "status": "good"
38 + },
39 + {
40 + "id": "switch-13",
41 + "type": "pkt",
42 + "status": "good"
43 + }
44 + ],
45 + "links": [
46 + { "src": "switch-1", "dst": "switch-2" },
47 + { "src": "switch-1", "dst": "switch-3" },
48 + { "src": "switch-1", "dst": "switch-4" },
49 + { "src": "switch-2", "dst": "switch-3" },
50 + { "src": "switch-2", "dst": "switch-4" },
51 + { "src": "switch-3", "dst": "switch-4" },
52 + { "src": "switch-13", "dst": "switch-3" },
53 + { "src": "switch-12", "dst": "switch-2" },
54 + { "src": "switch-11", "dst": "switch-1" }
55 + ]
56 +}
1 +/*
2 + ONOS CSS file
3 +
4 + @author Simon Hunt
5 + */
6 +
7 +body, html {
8 + height: 100%;
9 +}
10 +
11 +/*
12 + * Classes
13 + */
14 +
15 +span.title {
16 + color: red;
17 + font-size: 16pt;
18 + font-style: italic;
19 +}
20 +
21 +span.radio {
22 + color: darkslateblue;
23 +}
24 +
25 +span.right {
26 + float: right;
27 +}
28 +
29 +/*
30 + * === DEBUGGING ======
31 + */
32 +svg {
33 + border: 1px dashed red;
34 +}
35 +
36 +
37 +/*
38 + * Network Graph elements ======================================
39 + */
40 +
41 +.link {
42 + fill: none;
43 + stroke: #666;
44 + stroke-width: 1.5px;
45 + opacity: .7;
46 + /*marker-end: url(#end);*/
47 +
48 + transition: opacity 250ms;
49 + -webkit-transition: opacity 250ms;
50 + -moz-transition: opacity 250ms;
51 +}
52 +
53 +marker#end {
54 + fill: #666;
55 + stroke: #666;
56 + stroke-width: 1.5px;
57 +}
58 +
59 +.node rect {
60 + stroke-width: 1.5px;
61 +
62 + transition: opacity 250ms;
63 + -webkit-transition: opacity 250ms;
64 + -moz-transition: opacity 250ms;
65 +}
66 +
67 +.node text {
68 + fill: #000;
69 + font: 10px sans-serif;
70 + pointer-events: none;
71 +}
72 +
73 +.node.selected rect {
74 + filter: url(#blue-glow);
75 +}
76 +
77 +.link.inactive,
78 +.node.inactive rect,
79 +.node.inactive text {
80 + opacity: .2;
81 +}
82 +
83 +.node.inactive.selected rect,
84 +.node.inactive.selected text {
85 + opacity: .6;
86 +}
87 +
88 +.legend {
89 + position: fixed;
90 +}
91 +
92 +.legend .category rect {
93 + stroke-width: 1px;
94 +}
95 +
96 +.legend .category text {
97 + fill: #000;
98 + font: 10px sans-serif;
99 + pointer-events: none;
100 +}
101 +
102 +/*
103 + * =============================================================
104 + */
105 +
106 +/*
107 + * Specific structural elements
108 + */
109 +
110 +#frame {
111 + width: 100%;
112 + height: 100%;
113 + background-color: #ffd;
114 +}
115 +
116 +#mast {
117 + height: 32px;
118 + background-color: #dda;
119 + vertical-align: baseline;
120 +}
121 +
122 +#main {
123 + background-color: #99b;
124 +}
1 +/*
2 + ONOS UI Framework.
3 +
4 + @author Simon Hunt
5 + */
6 +
7 +(function ($) {
8 + 'use strict';
9 + var tsI = new Date().getTime(), // initialize time stamp
10 + tsB; // build time stamp
11 +
12 + // attach our main function to the jQuery object
13 + $.onos = function (options) {
14 + // private namespaces
15 + var publicApi; // public api
16 +
17 + // internal state
18 + var views = {},
19 + currentView = null,
20 + built = false;
21 +
22 + // DOM elements etc.
23 + var $mast;
24 +
25 +
26 + // various functions..................
27 +
28 + // throw an error
29 + function throwError(msg) {
30 + // todo: maybe add tracing later
31 + throw new Error(msg);
32 + }
33 +
34 + // define all the public api functions...
35 + publicApi = {
36 + printTime: function () {
37 + console.log("the time is " + new Date());
38 + },
39 +
40 + addView: function (vid, cb) {
41 + views[vid] = {
42 + vid: vid,
43 + cb: cb
44 + };
45 + // TODO: proper registration of views
46 + // for now, make the one (and only) view current..
47 + currentView = views[vid];
48 + }
49 + };
50 +
51 + // function to be called from index.html to build the ONOS UI
52 + function buildOnosUi() {
53 + tsB = new Date().getTime();
54 + tsI = tsB - tsI; // initialization duration
55 +
56 + console.log('ONOS UI initialized in ' + tsI + 'ms');
57 +
58 + if (built) {
59 + throwError("ONOS UI already built!");
60 + }
61 + built = true;
62 +
63 + // TODO: invoke hash navigation
64 + // --- report build errors ---
65 +
66 + // for now, invoke the one and only load function:
67 +
68 + currentView.cb.load();
69 + }
70 +
71 +
72 + // export the api and build-UI function
73 + return {
74 + api: publicApi,
75 + buildUi: buildOnosUi
76 + };
77 + };
78 +
79 +}(jQuery));
...\ No newline at end of file ...\ No newline at end of file