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;
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<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,8 +113,55 @@ public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListe
@Override
public void onMessage(String 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 {
......@@ -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");
......
{
"event": "addHostIntent",
"sid": 1,
"payload": {
"one": "hostOne",
"two": "hostTwo"
}
}
{
"event": "showPath",
"sid": 1,
"payload": {
"intentId": "0x1234",
"path": {
"links": [ "1-2", "2-3" ],
"traffic": false
}
}
}
{
"event": "monitorIntent",
"sid": 2,
"payload": {
"intentId": "0x1234"
}
}
{
"event": "showPath",
"sid": 2,
"payload": {
"intentId": "0x1234",
"path": {
"links": [ "1-2", "2-3" ],
"traffic": true,
"srcLabel": "567 Mb",
"dstLabel": "6 Mb"
}
}
}
{
"event": "showPath",
"sid": 2,
"payload": {
"intentId": "0x1234",
"path": {
"links": [ "1-2", "2-3" ],
"traffic": true,
"srcLabel": "967 Mb",
"dstLabel": "65 Mb"
}
}
}
{
"event": "showPath",
"sid": 2,
"payload": {
"intentId": "0x1234",
"path": {
"links": [ "1-2", "2-3" ],
"traffic": false
}
}
}
{
"event": "cancelMonitorIntent",
"sid": 3,
"payload": {
"intentId": "0x1234"
}
}
......@@ -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)
......