Thomas Vachuska

Adding multi-selection to the GUI and sketching out GUI/Server interactions.

Added persistent meta-data; including node coordinates.
Added ability to request path and return one.

Change-Id: I3edbdf44bbb8d8133a5e5a1fd0660a3fa5a2d6a1
...@@ -23,7 +23,9 @@ import org.eclipse.jetty.websocket.WebSocket; ...@@ -23,7 +23,9 @@ import org.eclipse.jetty.websocket.WebSocket;
23 import org.onlab.onos.event.Event; 23 import org.onlab.onos.event.Event;
24 import org.onlab.onos.net.Annotations; 24 import org.onlab.onos.net.Annotations;
25 import org.onlab.onos.net.Device; 25 import org.onlab.onos.net.Device;
26 +import org.onlab.onos.net.DeviceId;
26 import org.onlab.onos.net.Link; 27 import org.onlab.onos.net.Link;
28 +import org.onlab.onos.net.Path;
27 import org.onlab.onos.net.device.DeviceEvent; 29 import org.onlab.onos.net.device.DeviceEvent;
28 import org.onlab.onos.net.device.DeviceService; 30 import org.onlab.onos.net.device.DeviceService;
29 import org.onlab.onos.net.link.LinkEvent; 31 import org.onlab.onos.net.link.LinkEvent;
...@@ -37,7 +39,11 @@ import org.onlab.onos.net.topology.TopologyVertex; ...@@ -37,7 +39,11 @@ import org.onlab.onos.net.topology.TopologyVertex;
37 import org.onlab.osgi.ServiceDirectory; 39 import org.onlab.osgi.ServiceDirectory;
38 40
39 import java.io.IOException; 41 import java.io.IOException;
42 +import java.util.HashMap;
43 +import java.util.Map;
44 +import java.util.Set;
40 45
46 +import static org.onlab.onos.net.DeviceId.deviceId;
41 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED; 47 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED;
42 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_REMOVED; 48 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_REMOVED;
43 import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED; 49 import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED;
...@@ -56,6 +62,12 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe ...@@ -56,6 +62,12 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe
56 62
57 private Connection connection; 63 private Connection connection;
58 64
65 + // TODO: extract into an external & durable state; good enough for now and demo
66 + private static Map<String, ObjectNode> metaUi = new HashMap<>();
67 +
68 + private static final String COMPACT = "%s/%s-%s/%s";
69 +
70 +
59 /** 71 /**
60 * Creates a new web-socket for serving data to GUI topology view. 72 * Creates a new web-socket for serving data to GUI topology view.
61 * 73 *
...@@ -101,8 +113,55 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe ...@@ -101,8 +113,55 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe
101 113
102 @Override 114 @Override
103 public void onMessage(String data) { 115 public void onMessage(String data) {
116 + try {
117 + ObjectNode event = (ObjectNode) mapper.reader().readTree(data);
118 + String type = event.path("event").asText("unknown");
119 + ObjectNode payload = (ObjectNode) event.path("payload");
120 +
121 + switch (type) {
122 + case "updateMeta":
123 + metaUi.put(payload.path("id").asText(), payload);
124 + break;
125 + case "requestPath":
126 + findPath(deviceId(payload.path("one").asText()),
127 + deviceId(payload.path("two").asText()));
128 + default:
129 + break;
130 + }
131 + } catch (IOException e) {
104 System.out.println("Received: " + data); 132 System.out.println("Received: " + data);
105 } 133 }
134 + }
135 +
136 + private void findPath(DeviceId one, DeviceId two) {
137 + Set<Path> paths = topologyService.getPaths(topologyService.currentTopology(),
138 + one, two);
139 + if (!paths.isEmpty()) {
140 + ObjectNode payload = mapper.createObjectNode();
141 + ArrayNode links = mapper.createArrayNode();
142 +
143 + Path path = paths.iterator().next();
144 + for (Link link : path.links()) {
145 + links.add(compactLinkString(link));
146 + }
147 +
148 + payload.set("links", links);
149 + sendMessage(envelope("showPath", payload));
150 + }
151 + // TODO: when no path, send a message to the client
152 + }
153 +
154 + /**
155 + * Returns a compact string representing the given link.
156 + *
157 + * @param link infrastructure link
158 + * @return formatted link string
159 + */
160 + public static String compactLinkString(Link link) {
161 + return String.format(COMPACT, link.src().deviceId(), link.src().port(),
162 + link.dst().deviceId(), link.dst().port());
163 + }
164 +
106 165
107 private void sendMessage(String data) { 166 private void sendMessage(String data) {
108 try { 167 try {
...@@ -130,7 +189,11 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe ...@@ -130,7 +189,11 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe
130 // Add labels, props and stuff the payload into envelope. 189 // Add labels, props and stuff the payload into envelope.
131 payload.set("labels", labels); 190 payload.set("labels", labels);
132 payload.set("props", props(device.annotations())); 191 payload.set("props", props(device.annotations()));
133 - payload.set("metaUi", mapper.createObjectNode()); 192 +
193 + ObjectNode meta = metaUi.get(device.id().toString());
194 + if (meta != null) {
195 + payload.set("metaUi", meta);
196 + }
134 197
135 String type = (event.type() == DEVICE_ADDED) ? "addDevice" : 198 String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
136 ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice"); 199 ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
......
1 +{
2 + "event": "addHostIntent",
3 + "sid": 1,
4 + "payload": {
5 + "one": "hostOne",
6 + "two": "hostTwo"
7 + }
8 +}
1 +{
2 + "event": "showPath",
3 + "sid": 1,
4 + "payload": {
5 + "intentId": "0x1234",
6 + "path": {
7 + "links": [ "1-2", "2-3" ],
8 + "traffic": false
9 + }
10 + }
11 +}
1 +{
2 + "event": "monitorIntent",
3 + "sid": 2,
4 + "payload": {
5 + "intentId": "0x1234"
6 + }
7 +}
1 +{
2 + "event": "showPath",
3 + "sid": 2,
4 + "payload": {
5 + "intentId": "0x1234",
6 + "path": {
7 + "links": [ "1-2", "2-3" ],
8 + "traffic": true,
9 + "srcLabel": "567 Mb",
10 + "dstLabel": "6 Mb"
11 + }
12 + }
13 +}
1 +{
2 + "event": "showPath",
3 + "sid": 2,
4 + "payload": {
5 + "intentId": "0x1234",
6 + "path": {
7 + "links": [ "1-2", "2-3" ],
8 + "traffic": true,
9 + "srcLabel": "967 Mb",
10 + "dstLabel": "65 Mb"
11 + }
12 + }
13 +}
1 +{
2 + "event": "showPath",
3 + "sid": 2,
4 + "payload": {
5 + "intentId": "0x1234",
6 + "path": {
7 + "links": [ "1-2", "2-3" ],
8 + "traffic": false
9 + }
10 + }
11 +}
1 +{
2 + "event": "cancelMonitorIntent",
3 + "sid": 3,
4 + "payload": {
5 + "intentId": "0x1234"
6 + }
7 +}
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
28 28
29 // configuration data 29 // configuration data
30 var config = { 30 var config = {
31 - useLiveData: false, 31 + useLiveData: true,
32 debugOn: false, 32 debugOn: false,
33 debug: { 33 debug: {
34 showNodeXY: true, 34 showNodeXY: true,
...@@ -120,7 +120,9 @@ ...@@ -120,7 +120,9 @@
120 B: toggleBg, 120 B: toggleBg,
121 L: cycleLabels, 121 L: cycleLabels,
122 P: togglePorts, 122 P: togglePorts,
123 - U: unpin 123 + U: unpin,
124 +
125 + X: requestPath
124 }; 126 };
125 127
126 // state variables 128 // state variables
...@@ -132,7 +134,11 @@ ...@@ -132,7 +134,11 @@
132 }, 134 },
133 webSock, 135 webSock,
134 labelIdx = 0, 136 labelIdx = 0,
135 - selected = {}, 137 +
138 + //selected = {},
139 + selectOrder = [],
140 + selections = {},
141 +
136 highlighted = null, 142 highlighted = null,
137 hovered = null, 143 hovered = null,
138 viewMode = 'showAll', 144 viewMode = 'showAll',
...@@ -239,6 +245,14 @@ ...@@ -239,6 +245,14 @@
239 view.alert('unpin() callback') 245 view.alert('unpin() callback')
240 } 246 }
241 247
248 + function requestPath(view) {
249 + var payload = {
250 + one: selections[selectOrder[0]].obj.id,
251 + two: selections[selectOrder[1]].obj.id
252 + }
253 + sendMessage('requestPath', payload);
254 + }
255 +
242 // ============================== 256 // ==============================
243 // Radio Button Callbacks 257 // Radio Button Callbacks
244 258
...@@ -287,7 +301,8 @@ ...@@ -287,7 +301,8 @@
287 addDevice: addDevice, 301 addDevice: addDevice,
288 updateDevice: updateDevice, 302 updateDevice: updateDevice,
289 removeDevice: removeDevice, 303 removeDevice: removeDevice,
290 - addLink: addLink 304 + addLink: addLink,
305 + showPath: showPath
291 }; 306 };
292 307
293 function addDevice(data) { 308 function addDevice(data) {
...@@ -326,6 +341,10 @@ ...@@ -326,6 +341,10 @@
326 } 341 }
327 } 342 }
328 343
344 + function showPath(data) {
345 + network.view.alert(data.event + "\n" + data.payload.links.length);
346 + }
347 +
329 // .... 348 // ....
330 349
331 function unknownEvent(data) { 350 function unknownEvent(data) {
...@@ -611,7 +630,7 @@ ...@@ -611,7 +630,7 @@
611 }, 630 },
612 631
613 send : function(text) { 632 send : function(text) {
614 - if (text != null && text.length > 0) { 633 + if (text != null) {
615 webSock._send(text); 634 webSock._send(text);
616 } 635 }
617 }, 636 },
...@@ -619,11 +638,93 @@ ...@@ -619,11 +638,93 @@
619 _send : function(message) { 638 _send : function(message) {
620 if (webSock.ws) { 639 if (webSock.ws) {
621 webSock.ws.send(message); 640 webSock.ws.send(message);
641 + } else {
642 + network.view.alert('no web socket open');
622 } 643 }
623 } 644 }
624 645
625 }; 646 };
626 647
648 + var sid = 0;
649 +
650 + function sendMessage(evType, payload) {
651 + var toSend = {
652 + event: evType,
653 + sid: ++sid,
654 + payload: payload
655 + };
656 + webSock.send(JSON.stringify(toSend));
657 + }
658 +
659 +
660 + // ==============================
661 + // Selection stuff
662 +
663 + function selectObject(obj, el) {
664 + var n,
665 + meta = d3.event.sourceEvent.metaKey;
666 +
667 + if (el) {
668 + n = d3.select(el);
669 + } else {
670 + node.each(function(d) {
671 + if (d == obj) {
672 + n = d3.select(el = this);
673 + }
674 + });
675 + }
676 + if (!n) return;
677 +
678 + if (meta && n.classed('selected')) {
679 + deselectObject(obj.id);
680 + //flyinPane(null);
681 + return;
682 + }
683 +
684 + if (!meta) {
685 + deselectAll();
686 + }
687 +
688 + // TODO: allow for mutli selections
689 + var selected = {
690 + obj : obj,
691 + el : el
692 + };
693 +
694 + selections[obj.id] = selected;
695 + selectOrder.push(obj.id);
696 +
697 + n.classed('selected', true);
698 + //flyinPane(obj);
699 + }
700 +
701 + function deselectObject(id) {
702 + var obj = selections[id];
703 + if (obj) {
704 + d3.select(obj.el).classed('selected', false);
705 + selections[id] = null;
706 + // TODO: use splice to remove element
707 + }
708 + //flyinPane(null);
709 + }
710 +
711 + function deselectAll() {
712 + // deselect all nodes in the network...
713 + node.classed('selected', false);
714 + selections = {};
715 + selectOrder = [];
716 + //flyinPane(null);
717 + }
718 +
719 +
720 + $('#view').on('click', function(e) {
721 + if (!$(e.target).closest('.node').length) {
722 + if (!e.metaKey) {
723 + deselectAll();
724 + }
725 + }
726 + });
727 +
627 // ============================== 728 // ==============================
628 // View life-cycle callbacks 729 // View life-cycle callbacks
629 730
...@@ -678,7 +779,7 @@ ...@@ -678,7 +779,7 @@
678 } 779 }
679 780
680 function selectCb(d, self) { 781 function selectCb(d, self) {
681 - // TODO: selectObject(d, self); 782 + selectObject(d, self);
682 } 783 }
683 784
684 function atDragEnd(d, self) { 785 function atDragEnd(d, self) {
...@@ -686,11 +787,21 @@ ...@@ -686,11 +787,21 @@
686 // if it is a device (not a host) 787 // if it is a device (not a host)
687 if (d.class === 'device') { 788 if (d.class === 'device') {
688 d.fixed = true; 789 d.fixed = true;
689 - d3.select(self).classed('fixed', true) 790 + d3.select(self).classed('fixed', true);
791 + tellServerCoords(d);
690 // TODO: send new [x,y] back to server, via websocket. 792 // TODO: send new [x,y] back to server, via websocket.
691 } 793 }
692 } 794 }
693 795
796 + function tellServerCoords(d) {
797 + sendMessage('updateMeta', {
798 + id: d.id,
799 + 'class': d.class,
800 + x: d.x,
801 + y: d.y
802 + });
803 + }
804 +
694 // set up the force layout 805 // set up the force layout
695 network.force = d3.layout.force() 806 network.force = d3.layout.force()
696 .size(forceDim) 807 .size(forceDim)
......