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
Showing
9 changed files
with
252 additions
and
8 deletions
... | @@ -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"); | ... | ... |
... | @@ -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) | ... | ... |
-
Please register or login to post a comment