diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java index e82303b73b..31c3f84079 100644 --- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java +++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java @@ -23,7 +23,9 @@ import org.eclipse.jetty.websocket.WebSocket; import org.onlab.onos.event.Event; import org.onlab.onos.net.Annotations; import org.onlab.onos.net.Device; +import org.onlab.onos.net.DeviceId; import org.onlab.onos.net.Link; +import org.onlab.onos.net.Path; import org.onlab.onos.net.device.DeviceEvent; import org.onlab.onos.net.device.DeviceService; import org.onlab.onos.net.link.LinkEvent; @@ -37,7 +39,11 @@ import org.onlab.onos.net.topology.TopologyVertex; import org.onlab.osgi.ServiceDirectory; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import static org.onlab.onos.net.DeviceId.deviceId; import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED; import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_REMOVED; import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED; @@ -56,6 +62,12 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe private Connection connection; + // TODO: extract into an external & durable state; good enough for now and demo + private static Map metaUi = new HashMap<>(); + + private static final String COMPACT = "%s/%s-%s/%s"; + + /** * Creates a new web-socket for serving data to GUI topology view. * @@ -101,9 +113,56 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe @Override public void onMessage(String data) { - System.out.println("Received: " + data); + try { + ObjectNode event = (ObjectNode) mapper.reader().readTree(data); + String type = event.path("event").asText("unknown"); + ObjectNode payload = (ObjectNode) event.path("payload"); + + switch (type) { + case "updateMeta": + metaUi.put(payload.path("id").asText(), payload); + break; + case "requestPath": + findPath(deviceId(payload.path("one").asText()), + deviceId(payload.path("two").asText())); + default: + break; + } + } catch (IOException e) { + System.out.println("Received: " + data); + } } + private void findPath(DeviceId one, DeviceId two) { + Set paths = topologyService.getPaths(topologyService.currentTopology(), + one, two); + if (!paths.isEmpty()) { + ObjectNode payload = mapper.createObjectNode(); + ArrayNode links = mapper.createArrayNode(); + + Path path = paths.iterator().next(); + for (Link link : path.links()) { + links.add(compactLinkString(link)); + } + + payload.set("links", links); + sendMessage(envelope("showPath", payload)); + } + // TODO: when no path, send a message to the client + } + + /** + * Returns a compact string representing the given link. + * + * @param link infrastructure link + * @return formatted link string + */ + public static String compactLinkString(Link link) { + return String.format(COMPACT, link.src().deviceId(), link.src().port(), + link.dst().deviceId(), link.dst().port()); + } + + private void sendMessage(String data) { try { connection.sendMessage(data); @@ -130,7 +189,11 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe // Add labels, props and stuff the payload into envelope. payload.set("labels", labels); payload.set("props", props(device.annotations())); - payload.set("metaUi", mapper.createObjectNode()); + + ObjectNode meta = metaUi.get(device.id().toString()); + if (meta != null) { + payload.set("metaUi", meta); + } String type = (event.type() == DEVICE_ADDED) ? "addDevice" : ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice"); diff --git a/web/gui/src/main/webapp/json/intent/ev_1_ui.json b/web/gui/src/main/webapp/json/intent/ev_1_ui.json new file mode 100644 index 0000000000..962fcaa3d5 --- /dev/null +++ b/web/gui/src/main/webapp/json/intent/ev_1_ui.json @@ -0,0 +1,8 @@ +{ + "event": "addHostIntent", + "sid": 1, + "payload": { + "one": "hostOne", + "two": "hostTwo" + } +} diff --git a/web/gui/src/main/webapp/json/intent/ev_2_onos.json b/web/gui/src/main/webapp/json/intent/ev_2_onos.json new file mode 100644 index 0000000000..2b3bbe5e9f --- /dev/null +++ b/web/gui/src/main/webapp/json/intent/ev_2_onos.json @@ -0,0 +1,11 @@ +{ + "event": "showPath", + "sid": 1, + "payload": { + "intentId": "0x1234", + "path": { + "links": [ "1-2", "2-3" ], + "traffic": false + } + } +} diff --git a/web/gui/src/main/webapp/json/intent/ev_3_ui.json b/web/gui/src/main/webapp/json/intent/ev_3_ui.json new file mode 100644 index 0000000000..3151c8d981 --- /dev/null +++ b/web/gui/src/main/webapp/json/intent/ev_3_ui.json @@ -0,0 +1,7 @@ +{ + "event": "monitorIntent", + "sid": 2, + "payload": { + "intentId": "0x1234" + } +} diff --git a/web/gui/src/main/webapp/json/intent/ev_4_onos.json b/web/gui/src/main/webapp/json/intent/ev_4_onos.json new file mode 100644 index 0000000000..1a6632eba5 --- /dev/null +++ b/web/gui/src/main/webapp/json/intent/ev_4_onos.json @@ -0,0 +1,13 @@ +{ + "event": "showPath", + "sid": 2, + "payload": { + "intentId": "0x1234", + "path": { + "links": [ "1-2", "2-3" ], + "traffic": true, + "srcLabel": "567 Mb", + "dstLabel": "6 Mb" + } + } +} diff --git a/web/gui/src/main/webapp/json/intent/ev_5_onos.json b/web/gui/src/main/webapp/json/intent/ev_5_onos.json new file mode 100644 index 0000000000..7fd0ee41c6 --- /dev/null +++ b/web/gui/src/main/webapp/json/intent/ev_5_onos.json @@ -0,0 +1,13 @@ +{ + "event": "showPath", + "sid": 2, + "payload": { + "intentId": "0x1234", + "path": { + "links": [ "1-2", "2-3" ], + "traffic": true, + "srcLabel": "967 Mb", + "dstLabel": "65 Mb" + } + } +} diff --git a/web/gui/src/main/webapp/json/intent/ev_6_onos.json b/web/gui/src/main/webapp/json/intent/ev_6_onos.json new file mode 100644 index 0000000000..be3925e7ca --- /dev/null +++ b/web/gui/src/main/webapp/json/intent/ev_6_onos.json @@ -0,0 +1,11 @@ +{ + "event": "showPath", + "sid": 2, + "payload": { + "intentId": "0x1234", + "path": { + "links": [ "1-2", "2-3" ], + "traffic": false + } + } +} diff --git a/web/gui/src/main/webapp/json/intent/ev_7_ui.json b/web/gui/src/main/webapp/json/intent/ev_7_ui.json new file mode 100644 index 0000000000..158476e7dd --- /dev/null +++ b/web/gui/src/main/webapp/json/intent/ev_7_ui.json @@ -0,0 +1,7 @@ +{ + "event": "cancelMonitorIntent", + "sid": 3, + "payload": { + "intentId": "0x1234" + } +} diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js index b1901baf17..fc7c35af7e 100644 --- a/web/gui/src/main/webapp/topo2.js +++ b/web/gui/src/main/webapp/topo2.js @@ -28,7 +28,7 @@ // configuration data var config = { - useLiveData: false, + useLiveData: true, debugOn: false, debug: { showNodeXY: true, @@ -120,7 +120,9 @@ B: toggleBg, L: cycleLabels, P: togglePorts, - U: unpin + U: unpin, + + X: requestPath }; // state variables @@ -132,7 +134,11 @@ }, webSock, labelIdx = 0, - selected = {}, + + //selected = {}, + selectOrder = [], + selections = {}, + highlighted = null, hovered = null, viewMode = 'showAll', @@ -239,6 +245,14 @@ view.alert('unpin() callback') } + function requestPath(view) { + var payload = { + one: selections[selectOrder[0]].obj.id, + two: selections[selectOrder[1]].obj.id + } + sendMessage('requestPath', payload); + } + // ============================== // Radio Button Callbacks @@ -287,7 +301,8 @@ addDevice: addDevice, updateDevice: updateDevice, removeDevice: removeDevice, - addLink: addLink + addLink: addLink, + showPath: showPath }; function addDevice(data) { @@ -326,6 +341,10 @@ } } + function showPath(data) { + network.view.alert(data.event + "\n" + data.payload.links.length); + } + // .... function unknownEvent(data) { @@ -611,7 +630,7 @@ }, send : function(text) { - if (text != null && text.length > 0) { + if (text != null) { webSock._send(text); } }, @@ -619,11 +638,93 @@ _send : function(message) { if (webSock.ws) { webSock.ws.send(message); + } else { + network.view.alert('no web socket open'); } } }; + var sid = 0; + + function sendMessage(evType, payload) { + var toSend = { + event: evType, + sid: ++sid, + payload: payload + }; + webSock.send(JSON.stringify(toSend)); + } + + + // ============================== + // Selection stuff + + function selectObject(obj, el) { + var n, + meta = d3.event.sourceEvent.metaKey; + + if (el) { + n = d3.select(el); + } else { + node.each(function(d) { + if (d == obj) { + n = d3.select(el = this); + } + }); + } + if (!n) return; + + if (meta && n.classed('selected')) { + deselectObject(obj.id); + //flyinPane(null); + return; + } + + if (!meta) { + deselectAll(); + } + + // TODO: allow for mutli selections + var selected = { + obj : obj, + el : el + }; + + selections[obj.id] = selected; + selectOrder.push(obj.id); + + n.classed('selected', true); + //flyinPane(obj); + } + + function deselectObject(id) { + var obj = selections[id]; + if (obj) { + d3.select(obj.el).classed('selected', false); + selections[id] = null; + // TODO: use splice to remove element + } + //flyinPane(null); + } + + function deselectAll() { + // deselect all nodes in the network... + node.classed('selected', false); + selections = {}; + selectOrder = []; + //flyinPane(null); + } + + + $('#view').on('click', function(e) { + if (!$(e.target).closest('.node').length) { + if (!e.metaKey) { + deselectAll(); + } + } + }); + // ============================== // View life-cycle callbacks @@ -678,7 +779,7 @@ } function selectCb(d, self) { - // TODO: selectObject(d, self); + selectObject(d, self); } function atDragEnd(d, self) { @@ -686,11 +787,21 @@ // if it is a device (not a host) if (d.class === 'device') { d.fixed = true; - d3.select(self).classed('fixed', true) + d3.select(self).classed('fixed', true); + tellServerCoords(d); // TODO: send new [x,y] back to server, via websocket. } } + function tellServerCoords(d) { + sendMessage('updateMeta', { + id: d.id, + 'class': d.class, + x: d.x, + y: d.y + }); + } + // set up the force layout network.force = d3.layout.force() .size(forceDim)