Simon Hunt

GUI -- Added onos.ui.addFloatingPanel() function.

- re-instated detail pane in topo2.js; triggered of non-zero selection state.
- single-select now requests details and displays them in detail pane.
- multi-select WIP.

Change-Id: I300a3dfd4d35abc82f832a172854c6aff50d8cd6
1 +/*
2 + * Copyright 2014 Open Networking Laboratory
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + */
16 +
17 +/*
18 + ONOS GUI -- Floating Panels -- CSS file
19 +
20 + @author Simon Hunt
21 + */
22 +
23 +.fpanel {
24 + position: absolute;
25 + z-index: 100;
26 + display: block;
27 + top: 10%;
28 + width: 280px;
29 + right: -300px;
30 + opacity: 0;
31 + background-color: rgba(255,255,255,0.8);
32 +
33 + padding: 10px;
34 + color: black;
35 + font-size: 10pt;
36 + box-shadow: 2px 2px 16px #777;
37 +}
38 +
39 +/* TODO: light/dark themes */
40 +.light .fpanel {
41 +
42 +}
43 +.dark .fpanel {
44 +
45 +}
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
40 <link rel="stylesheet" href="base.css"> 40 <link rel="stylesheet" href="base.css">
41 <link rel="stylesheet" href="onos2.css"> 41 <link rel="stylesheet" href="onos2.css">
42 <link rel="stylesheet" href="mast2.css"> 42 <link rel="stylesheet" href="mast2.css">
43 + <link rel="stylesheet" href="floatPanel.css">
43 44
44 <!-- This is where contributed stylesheets get INJECTED --> 45 <!-- This is where contributed stylesheets get INJECTED -->
45 <!-- TODO: replace with template marker and inject refs server-side --> 46 <!-- TODO: replace with template marker and inject refs server-side -->
...@@ -62,8 +63,9 @@ ...@@ -62,8 +63,9 @@
62 <div id="view"> 63 <div id="view">
63 <!-- NOTE: views injected here by onos.js --> 64 <!-- NOTE: views injected here by onos.js -->
64 </div> 65 </div>
65 - <div id="overlays"> 66 + <div id="floatPanels">
66 - <!-- NOTE: overlays injected here, as needed --> 67 + <!-- NOTE: floating panels injected here, as needed -->
68 + <!-- see onos.ui.addFloatingPanel -->
67 </div> 69 </div>
68 <div id="alerts"> 70 <div id="alerts">
69 <!-- NOTE: alert content injected here, as needed --> 71 <!-- NOTE: alert content injected here, as needed -->
......
1 +{
2 + "event": "showDetails",
3 + "sid": 9,
4 + "payload": {
5 + "id": "CA:4B:EE:A4:B0:33/-1",
6 + "type": "host",
7 + "propOrder": [
8 + "MAC",
9 + "IP",
10 + "-",
11 + "Latitude",
12 + "Longitude"
13 + ],
14 + "props": {
15 + "MAC": "CA:4B:EE:A4:B0:33",
16 + "IP": "[10.0.0.1]",
17 + "-": "",
18 + "Latitude": null,
19 + "Longitude": null
20 + }
21 + }
22 +}
1 +{
2 + "event": "showDetails",
3 + "sid": 37,
4 + "payload": {
5 + "id": "of:000000000000000a",
6 + "type": "switch",
7 + "propOrder": [
8 + "Name",
9 + "Vendor",
10 + "H/W Version",
11 + "S/W Version",
12 + "Serial Number",
13 + "-",
14 + "Latitude",
15 + "Longitude",
16 + "Ports",
17 + "-",
18 + "Master"
19 + ],
20 + "props": {
21 + "Name": null,
22 + "Vendor": "Nicira, Inc.",
23 + "H/W Version": "Open vSwitch",
24 + "S/W Version": "2.0.1",
25 + "Serial Number": "None",
26 + "-": "",
27 + "Latitude": null,
28 + "Longitude": null,
29 + "Ports": "5",
30 + "Master":"local"
31 + }
32 + }
33 +}
1 +{
2 + "event": "requestDetails",
3 + "sid": 15,
4 + "payload": {
5 + "id": "of:0000000000000003",
6 + "class": "device"
7 + }
8 +}
9 +
1 +{
2 + "event": "requestDetails",
3 + "sid": 9,
4 + "payload": {
5 + "id": "CA:4B:EE:A4:B0:33/-1",
6 + "class": "host"
7 + }
8 +}
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
50 50
51 // internal state 51 // internal state
52 var views = {}, 52 var views = {},
53 + fpanels = {},
53 current = { 54 current = {
54 view: null, 55 view: null,
55 ctx: '', 56 ctx: '',
...@@ -57,7 +58,7 @@ ...@@ -57,7 +58,7 @@
57 theme: settings.theme 58 theme: settings.theme
58 }, 59 },
59 built = false, 60 built = false,
60 - errorCount = 0, 61 + buildErrors = [],
61 keyHandler = { 62 keyHandler = {
62 globalKeys: {}, 63 globalKeys: {},
63 maskedKeys: {}, 64 maskedKeys: {},
...@@ -70,7 +71,11 @@ ...@@ -70,7 +71,11 @@
70 }; 71 };
71 72
72 // DOM elements etc. 73 // DOM elements etc.
73 - var $view, 74 + // TODO: verify existence of following elements...
75 + var $view = d3.select('#view'),
76 + $floatPanels = d3.select('#floatPanels'),
77 + $alerts = d3.select('#alerts'),
78 + // note, following elements added programmatically...
74 $mastRadio; 79 $mastRadio;
75 80
76 81
...@@ -241,10 +246,22 @@ ...@@ -241,10 +246,22 @@
241 setView(view, hash, t); 246 setView(view, hash, t);
242 } 247 }
243 248
249 + function buildError(msg) {
250 + buildErrors.push(msg);
251 + }
252 +
244 function reportBuildErrors() { 253 function reportBuildErrors() {
245 traceFn('reportBuildErrors'); 254 traceFn('reportBuildErrors');
246 - // TODO: validate registered views / nav-item linkage etc. 255 + var nerr = buildErrors.length,
247 - console.log('(no build errors)'); 256 + errmsg;
257 + if (!nerr) {
258 + console.log('(no build errors)');
259 + } else {
260 + errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
261 + buildErrors.join('\n');
262 + doAlert(errmsg);
263 + console.error(errmsg);
264 + }
248 } 265 }
249 266
250 // returns the reference if it is a function, null otherwise 267 // returns the reference if it is a function, null otherwise
...@@ -449,22 +466,20 @@ ...@@ -449,22 +466,20 @@
449 } 466 }
450 467
451 function createAlerts() { 468 function createAlerts() {
452 - var al = d3.select('#alerts') 469 + $alerts.style('display', 'block');
453 - .style('display', 'block'); 470 + $alerts.append('span')
454 - al.append('span')
455 .attr('class', 'close') 471 .attr('class', 'close')
456 .text('X') 472 .text('X')
457 .on('click', closeAlerts); 473 .on('click', closeAlerts);
458 - al.append('pre'); 474 + $alerts.append('pre');
459 - al.append('p').attr('class', 'footnote') 475 + $alerts.append('p').attr('class', 'footnote')
460 .text('Press ESCAPE to close'); 476 .text('Press ESCAPE to close');
461 alerts.open = true; 477 alerts.open = true;
462 alerts.count = 0; 478 alerts.count = 0;
463 } 479 }
464 480
465 function closeAlerts() { 481 function closeAlerts() {
466 - d3.select('#alerts') 482 + $alerts.style('display', 'none')
467 - .style('display', 'none')
468 .html(''); 483 .html('');
469 alerts.open = false; 484 alerts.open = false;
470 } 485 }
...@@ -474,7 +489,7 @@ ...@@ -474,7 +489,7 @@
474 oldContent; 489 oldContent;
475 490
476 if (alerts.count) { 491 if (alerts.count) {
477 - oldContent = d3.select('#alerts pre').html(); 492 + oldContent = $alerts.select('pre').html();
478 } 493 }
479 494
480 lines = msg.split('\n'); 495 lines = msg.split('\n');
...@@ -485,7 +500,7 @@ ...@@ -485,7 +500,7 @@
485 lines += '\n----\n' + oldContent; 500 lines += '\n----\n' + oldContent;
486 } 501 }
487 502
488 - d3.select('#alerts pre').html(lines); 503 + $alerts.select('pre').html(lines);
489 alerts.count++; 504 alerts.count++;
490 } 505 }
491 506
...@@ -691,6 +706,53 @@ ...@@ -691,6 +706,53 @@
691 libApi[libName] = api; 706 libApi[libName] = api;
692 }, 707 },
693 708
709 + // TODO: implement floating panel as a class
710 + // TODO: parameterize position (currently hard-coded to TopRight)
711 + /*
712 + * Creates div in floating panels block, with the given id.
713 + * Returns panel token used to interact with the panel
714 + */
715 + addFloatingPanel: function (id, position) {
716 + var pos = position || 'TR',
717 + el,
718 + fp;
719 +
720 + if (fpanels[id]) {
721 + buildError('Float panel with id "' + id + '" already exists.');
722 + return null;
723 + }
724 +
725 + el = $floatPanels.append('div')
726 + .attr('id', id)
727 + .attr('class', 'fpanel');
728 +
729 + fp = {
730 + id: id,
731 + el: el,
732 + pos: pos,
733 + show: function () {
734 + console.log('show pane: ' + id);
735 + el.transition().duration(750)
736 + .style('right', '20px')
737 + .style('opacity', 1);
738 + },
739 + hide: function () {
740 + console.log('hide pane: ' + id);
741 + el.transition().duration(750)
742 + .style('right', '-320px')
743 + .style('opacity', 0);
744 + },
745 + empty: function () {
746 + return el.html('');
747 + },
748 + append: function (what) {
749 + return el.append(what);
750 + }
751 + };
752 + fpanels[id] = fp;
753 + return fp;
754 + },
755 +
694 // TODO: it remains to be seen whether we keep this style of docs 756 // TODO: it remains to be seen whether we keep this style of docs
695 /** @api ui addView( vid, nid, cb ) 757 /** @api ui addView( vid, nid, cb )
696 * Adds a view to the UI. 758 * Adds a view to the UI.
...@@ -782,7 +844,6 @@ ...@@ -782,7 +844,6 @@
782 } 844 }
783 built = true; 845 built = true;
784 846
785 - $view = d3.select('#view');
786 $mastRadio = d3.select('#mastRadio'); 847 $mastRadio = d3.select('#mastRadio');
787 848
788 $(window).on('hashchange', hash); 849 $(window).on('hashchange', hash);
......
...@@ -96,3 +96,45 @@ ...@@ -96,3 +96,45 @@
96 fill: white; 96 fill: white;
97 stroke: red; 97 stroke: red;
98 } 98 }
99 +
100 +
101 +/* detail topo-detail pane */
102 +
103 +#topo-detail {
104 +/* gets base CSS from .fpanel in floatPanel.css */
105 +}
106 +
107 +
108 +#topo-detail h2 {
109 + margin: 8px 4px;
110 + color: black;
111 + vertical-align: middle;
112 +}
113 +
114 +#topo-detail h2 img {
115 + height: 32px;
116 + padding-right: 8px;
117 + vertical-align: middle;
118 +}
119 +
120 +#topo-detail p, table {
121 + margin: 4px 4px;
122 +}
123 +
124 +#topo-detail td.label {
125 + font-style: italic;
126 + color: #777;
127 + padding-right: 12px;
128 +}
129 +
130 +#topo-detail td.value {
131 +
132 +}
133 +
134 +#topo-detail hr {
135 + height: 1px;
136 + color: #ccc;
137 + background-color: #ccc;
138 + border: 0;
139 +}
140 +
......
...@@ -152,7 +152,7 @@ ...@@ -152,7 +152,7 @@
152 webSock, 152 webSock,
153 deviceLabelIndex = 0, 153 deviceLabelIndex = 0,
154 hostLabelIndex = 0, 154 hostLabelIndex = 0,
155 - 155 + detailPane,
156 selectOrder = [], 156 selectOrder = [],
157 selections = {}, 157 selections = {},
158 158
...@@ -192,6 +192,10 @@ ...@@ -192,6 +192,10 @@
192 192
193 function testMe(view) { 193 function testMe(view) {
194 view.alert('test'); 194 view.alert('test');
195 + detailPane.show();
196 + setTimeout(function () {
197 + detailPane.hide();
198 + }, 3000);
195 } 199 }
196 200
197 function abortIfLive() { 201 function abortIfLive() {
...@@ -285,14 +289,6 @@ ...@@ -285,14 +289,6 @@
285 view.alert('unpin() callback') 289 view.alert('unpin() callback')
286 } 290 }
287 291
288 - function requestPath(view) {
289 - var payload = {
290 - one: selections[selectOrder[0]].obj.id,
291 - two: selections[selectOrder[1]].obj.id
292 - }
293 - sendMessage('requestPath', payload);
294 - }
295 -
296 // ============================== 292 // ==============================
297 // Radio Button Callbacks 293 // Radio Button Callbacks
298 294
...@@ -353,6 +349,7 @@ ...@@ -353,6 +349,7 @@
353 removeDevice: stillToImplement, 349 removeDevice: stillToImplement,
354 removeLink: removeLink, 350 removeLink: removeLink,
355 removeHost: removeHost, 351 removeHost: removeHost,
352 + showDetails: showDetails,
356 showPath: showPath 353 showPath: showPath
357 }; 354 };
358 355
...@@ -463,6 +460,12 @@ ...@@ -463,6 +460,12 @@
463 } 460 }
464 } 461 }
465 462
463 + function showDetails(data) {
464 + fnTrace('showDetails', data.payload.id);
465 + populateDetails(data.payload);
466 + detailPane.show();
467 + }
468 +
466 function showPath(data) { 469 function showPath(data) {
467 fnTrace('showPath', data.payload.id); 470 fnTrace('showPath', data.payload.id);
468 var links = data.payload.links, 471 var links = data.payload.links,
...@@ -500,6 +503,32 @@ ...@@ -500,6 +503,32 @@
500 } 503 }
501 504
502 // ============================== 505 // ==============================
506 + // Out-going messages...
507 +
508 + function getSel(idx) {
509 + return selections[selectOrder[idx]];
510 + }
511 +
512 + // for now, just a host-to-host intent, (and implicit start-monitoring)
513 + function requestPath() {
514 + var payload = {
515 + one: getSel(0).obj.id,
516 + two: getSel(1).obj.id
517 + };
518 + sendMessage('requestPath', payload);
519 + }
520 +
521 + // request details for the selected element
522 + function requestDetails() {
523 + var data = getSel(0).obj,
524 + payload = {
525 + id: data.id,
526 + class: data.class
527 + };
528 + sendMessage('requestDetails', payload);
529 + }
530 +
531 + // ==============================
503 // force layout modification functions 532 // force layout modification functions
504 533
505 function translate(x, y) { 534 function translate(x, y) {
...@@ -1015,6 +1044,8 @@ ...@@ -1015,6 +1044,8 @@
1015 1044
1016 var sid = 0; 1045 var sid = 0;
1017 1046
1047 + // TODO: use cache of pending messages (key = sid) to reconcile responses
1048 +
1018 function sendMessage(evType, payload) { 1049 function sendMessage(evType, payload) {
1019 var toSend = { 1050 var toSend = {
1020 event: evType, 1051 event: evType,
...@@ -1033,7 +1064,6 @@ ...@@ -1033,7 +1064,6 @@
1033 wsTrace('rx', msg); 1064 wsTrace('rx', msg);
1034 } 1065 }
1035 function wsTrace(rxtx, msg) { 1066 function wsTrace(rxtx, msg) {
1036 -
1037 console.log('[' + rxtx + '] ' + msg); 1067 console.log('[' + rxtx + '] ' + msg);
1038 // TODO: integrate with trace view 1068 // TODO: integrate with trace view
1039 //if (trace) { 1069 //if (trace) {
...@@ -1062,7 +1092,7 @@ ...@@ -1062,7 +1092,7 @@
1062 1092
1063 if (meta && n.classed('selected')) { 1093 if (meta && n.classed('selected')) {
1064 deselectObject(obj.id); 1094 deselectObject(obj.id);
1065 - //flyinPane(null); 1095 + updateDetailPane();
1066 return; 1096 return;
1067 } 1097 }
1068 1098
...@@ -1074,17 +1104,16 @@ ...@@ -1074,17 +1104,16 @@
1074 selectOrder.push(obj.id); 1104 selectOrder.push(obj.id);
1075 1105
1076 n.classed('selected', true); 1106 n.classed('selected', true);
1077 - //flyinPane(obj); 1107 + updateDetailPane();
1078 } 1108 }
1079 1109
1080 function deselectObject(id) { 1110 function deselectObject(id) {
1081 var obj = selections[id]; 1111 var obj = selections[id];
1082 if (obj) { 1112 if (obj) {
1083 d3.select(obj.el).classed('selected', false); 1113 d3.select(obj.el).classed('selected', false);
1084 - selections[id] = null; 1114 + delete selections[id];
1085 - // TODO: use splice to remove element
1086 } 1115 }
1087 - //flyinPane(null); 1116 + updateDetailPane();
1088 } 1117 }
1089 1118
1090 function deselectAll() { 1119 function deselectAll() {
...@@ -1092,10 +1121,10 @@ ...@@ -1092,10 +1121,10 @@
1092 node.classed('selected', false); 1121 node.classed('selected', false);
1093 selections = {}; 1122 selections = {};
1094 selectOrder = []; 1123 selectOrder = [];
1095 - //flyinPane(null); 1124 + updateDetailPane();
1096 } 1125 }
1097 1126
1098 - // TODO: this click handler does not get unloaded when the view does 1127 + // FIXME: this click handler does not get unloaded when the view does
1099 $('#view').on('click', function(e) { 1128 $('#view').on('click', function(e) {
1100 if (!$(e.target).closest('.node').length) { 1129 if (!$(e.target).closest('.node').length) {
1101 if (!e.metaKey) { 1130 if (!e.metaKey) {
...@@ -1104,6 +1133,66 @@ ...@@ -1104,6 +1133,66 @@
1104 } 1133 }
1105 }); 1134 });
1106 1135
1136 + // update the state of the detail pane, based on current selections
1137 + function updateDetailPane() {
1138 + var nSel = selectOrder.length;
1139 + if (!nSel) {
1140 + detailPane.hide();
1141 + } else if (nSel === 1) {
1142 + singleSelect();
1143 + } else {
1144 + multiSelect();
1145 + }
1146 + }
1147 +
1148 + function singleSelect() {
1149 + requestDetails();
1150 + // NOTE: detail pane will be shown from showDetails event.
1151 + }
1152 +
1153 + function multiSelect() {
1154 + // TODO: use detail pane for multi-select view.
1155 + //detailPane.show();
1156 + }
1157 +
1158 + function populateDetails(data) {
1159 + detailPane.empty();
1160 +
1161 + var title = detailPane.append("h2"),
1162 + table = detailPane.append("table"),
1163 + tbody = table.append("tbody");
1164 +
1165 + $('<img src="img/' + data.type + '.png">').appendTo(title);
1166 + $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
1167 +
1168 + data.propOrder.forEach(function(p) {
1169 + if (p === '-') {
1170 + addSep(tbody);
1171 + } else {
1172 + addProp(tbody, p, data.props[p]);
1173 + }
1174 + });
1175 +
1176 + function addSep(tbody) {
1177 + var tr = tbody.append('tr');
1178 + $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1179 + }
1180 +
1181 + function addProp(tbody, label, value) {
1182 + var tr = tbody.append('tr');
1183 +
1184 + tr.append('td')
1185 + .attr('class', 'label')
1186 + .text(label + ' :');
1187 +
1188 + tr.append('td')
1189 + .attr('class', 'value')
1190 + .text(value);
1191 + }
1192 + }
1193 +
1194 + // ==============================
1195 + // Test harness code
1107 1196
1108 function prepareScenario(view, ctx, dbg) { 1197 function prepareScenario(view, ctx, dbg) {
1109 var sc = scenario, 1198 var sc = scenario,
...@@ -1272,4 +1361,6 @@ ...@@ -1272,4 +1361,6 @@
1272 resize: resize 1361 resize: resize
1273 }); 1362 });
1274 1363
1364 + detailPane = onos.ui.addFloatingPanel('topo-detail');
1365 +
1275 }(ONOS)); 1366 }(ONOS));
......