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
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 e82303b..31c3f84 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.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.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 @@
private Connection connection;
+ // TODO: extract into an external & durable state; good enough for now and demo
+ private static Map<String, ObjectNode> 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 @@
@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<Path> 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 @@
// 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 0000000..962fcaa
--- /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 0000000..2b3bbe5
--- /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 0000000..3151c8d
--- /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 0000000..1a6632e
--- /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 0000000..7fd0ee4
--- /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 0000000..be3925e
--- /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 0000000..158476e
--- /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 b1901ba..fc7c35a 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)