Simon Hunt
Committed by Gerrit Code Review

ONOS-1479 -- GUI - augmenting topology view for extensibility:

- Added id field to property panel, as well as overloaded constructors.
- Added modify*Details() methods to UiTopoOverlay.
- Cleaned up use of string constants.
- Reworked RequestDetails in Topo view msg handler (and base).
- Fixed bug in topo UI where selected host title click caused exception on server.

Change-Id: Ib2a3cf60fae8ad8cda77a3b6933ee758262e6f3c
......@@ -38,9 +38,7 @@ public final class JsonUtils {
* @param sid sequence ID
* @param payload event payload
* @return the object node representation
* @deprecated in Cardinal Release
*/
@Deprecated
public static ObjectNode envelope(String type, long sid, ObjectNode payload) {
ObjectNode event = MAPPER.createObjectNode();
event.put("event", type);
......
......@@ -26,7 +26,10 @@ import org.slf4j.LoggerFactory;
*/
public class UiTopoOverlay {
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* Logger for this overlay.
*/
protected final Logger log = LoggerFactory.getLogger(getClass());
private final String id;
......@@ -72,7 +75,7 @@ public class UiTopoOverlay {
/**
* Callback invoked to destroy this instance by cleaning up any
* internal state ready for garbage collection.
* This default implementation does nothing.
* This default implementation holds no state and does nothing.
*/
public void destroy() {
}
......@@ -85,4 +88,24 @@ public class UiTopoOverlay {
*/
public void modifySummary(PropertyPanel pp) {
}
/**
* Callback to modify the contents of the details panel for
* a selected device.
* This default implementation does nothing.
*
* @param pp property panel model of summary data
*/
public void modifyDeviceDetails(PropertyPanel pp) {
}
/**
* Callback to modify the contents of the details panel for
* a selected host.
* This default implementation does nothing.
*
* @param pp property panel model of summary data
*/
public void modifyHostDetails(PropertyPanel pp) {
}
}
......
......@@ -19,6 +19,7 @@ package org.onosproject.ui.topo;
import com.google.common.collect.Sets;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
......@@ -30,6 +31,7 @@ public class PropertyPanel {
private String title;
private String typeId;
private String id;
private List<Prop> properties = new ArrayList<>();
/**
......@@ -56,6 +58,19 @@ public class PropertyPanel {
}
/**
* Adds an ID field to the panel data, to be included in
* the returned JSON data to the client.
*
* @param id the identifier
* @return self, for chaining
*/
public PropertyPanel id(String id) {
this.id = id;
return this;
}
/**
* Returns the title text.
*
* @return title text
......@@ -74,6 +89,15 @@ public class PropertyPanel {
}
/**
* Returns the internal ID.
*
* @return the ID
*/
public String id() {
return id;
}
/**
* Returns the list of properties to be displayed.
*
* @return the property list
......@@ -137,6 +161,8 @@ public class PropertyPanel {
// ====================
private static final DecimalFormat DF0 = new DecimalFormat("#,###");
/**
* Simple data carrier for a property, composed of a key/value pair.
*/
......@@ -156,6 +182,26 @@ public class PropertyPanel {
}
/**
* Constructs a property data value.
* @param key property key
* @param value property value
*/
public Prop(String key, int value) {
this.key = key;
this.value = DF0.format(value);
}
/**
* Constructs a property data value.
* @param key property key
* @param value property value
*/
public Prop(String key, long value) {
this.key = key;
this.value = DF0.format(value);
}
/**
* Returns the property's key.
*
* @return the key
......
......@@ -74,12 +74,20 @@ public class TopoOverlayCache {
return isNullOrEmpty(id) ? NONE : overlays.get(id);
}
/**
* Returns the current overlay instance.
* Note that this method always returns a reference; when there is no
* overlay selected the "NULL" overlay instance is returned.
*
* @return the current overlay
*/
public UiTopoOverlay currentOverlay() {
return current;
}
/**
* Returns the number of overlays in the cache.
* Returns the number of overlays in the cache. Remember that this
* includes the "NULL" overlay, representing "no overlay selected".
*
* @return number of overlays
*/
......@@ -88,16 +96,13 @@ public class TopoOverlayCache {
}
// overlay instance representing "no overlay selected"
private static class NullOverlay extends UiTopoOverlay {
public NullOverlay() {
super(null);
}
@Override
public void init() {
}
// override activate and deactivate, so no log messages are written
@Override
public void activate() {
}
......@@ -105,9 +110,5 @@ public class TopoOverlayCache {
@Override
public void deactivate() {
}
@Override
public void destroy() {
}
}
}
......
......@@ -85,6 +85,7 @@ import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
*/
public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
// incoming event types
private static final String REQ_DETAILS = "requestDetails";
private static final String UPDATE_META = "updateMeta";
private static final String ADD_HOST_INTENT = "addHostIntent";
......@@ -107,6 +108,33 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
private static final String TOPO_SELECT_OVERLAY = "topoSelectOverlay";
private static final String TOPO_STOP = "topoStop";
// outgoing event types
private static final String SHOW_SUMMARY = "showSummary";
private static final String SHOW_DETAILS = "showDetails";
private static final String SPRITE_LIST_RESPONSE = "spriteListResponse";
private static final String SPRITE_DATA_RESPONSE = "spriteDataResponse";
private static final String UPDATE_INSTANCE = "updateInstance";
// fields
private static final String ID = "id";
private static final String IDS = "ids";
private static final String HOVER = "hover";
private static final String DEVICE = "device";
private static final String HOST = "host";
private static final String CLASS = "class";
private static final String UNKNOWN = "unknown";
private static final String ONE = "one";
private static final String TWO = "two";
private static final String SRC = "src";
private static final String DST = "dst";
private static final String DATA = "data";
private static final String NAME = "name";
private static final String NAMES = "names";
private static final String ACTIVATE = "activate";
private static final String DEACTIVATE = "deactivate";
private static final String PRIMARY = "primary";
private static final String SECONDARY = "secondary";
private static final String APP_ID = "org.onosproject.gui";
......@@ -244,8 +272,8 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void process(long sid, ObjectNode payload) {
String deact = string(payload, "deactivate");
String act = string(payload, "activate");
String deact = string(payload, DEACTIVATE);
String act = string(payload, ACTIVATE);
overlayCache.switchOverlay(deact, act);
}
}
......@@ -295,8 +323,8 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
ObjectNode root = objectNode();
ArrayNode names = arrayNode();
get(SpriteService.class).getNames().forEach(names::add);
root.set("names", names);
sendMessage("spriteListResponse", sid, root);
root.set(NAMES, names);
sendMessage(SPRITE_LIST_RESPONSE, sid, root);
}
}
......@@ -307,10 +335,10 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void process(long sid, ObjectNode payload) {
String name = string(payload, "name");
String name = string(payload, NAME);
ObjectNode root = objectNode();
root.set("data", get(SpriteService.class).get(name));
sendMessage("spriteDataResponse", sid, root);
root.set(DATA, get(SpriteService.class).get(name));
sendMessage(SPRITE_DATA_RESPONSE, sid, root);
}
}
......@@ -321,14 +349,20 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void process(long sid, ObjectNode payload) {
String type = string(payload, "class", "unknown");
String id = string(payload, "id");
if (type.equals("device")) {
sendMessage(deviceDetails(deviceId(id), sid));
} else if (type.equals("host")) {
sendMessage(hostDetails(hostId(id), sid));
String type = string(payload, CLASS, UNKNOWN);
String id = string(payload, ID);
PropertyPanel pp = null;
if (type.equals(DEVICE)) {
pp = deviceDetails(deviceId(id), sid);
overlayCache.currentOverlay().modifyDeviceDetails(pp);
} else if (type.equals(HOST)) {
pp = hostDetails(hostId(id), sid);
overlayCache.currentOverlay().modifyHostDetails(pp);
}
ObjectNode json = JsonUtils.envelope(SHOW_DETAILS, sid, json(pp));
sendMessage(json);
}
}
......@@ -364,8 +398,8 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void process(long sid, ObjectNode payload) {
// TODO: add protection against device ids and non-existent hosts.
HostId one = hostId(string(payload, "one"));
HostId two = hostId(string(payload, "two"));
HostId one = hostId(string(payload, ONE));
HostId two = hostId(string(payload, TWO));
HostToHostIntent intent = HostToHostIntent.builder()
.appId(appId)
......@@ -386,8 +420,8 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void process(long sid, ObjectNode payload) {
// TODO: add protection against device ids and non-existent hosts.
Set<HostId> src = getHostIds((ArrayNode) payload.path("src"));
HostId dst = hostId(string(payload, "dst"));
Set<HostId> src = getHostIds((ArrayNode) payload.path(SRC));
HostId dst = hostId(string(payload, DST));
Host dstHost = hostService.getHost(dst);
Set<ConnectPoint> ingressPoints = getHostLocations(src);
......@@ -421,12 +455,12 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
// Cancel any other traffic monitoring mode.
stopTrafficMonitoring();
if (!payload.has("ids")) {
if (!payload.has(IDS)) {
return;
}
// Get the set of selected hosts and their intents.
ArrayNode ids = (ArrayNode) payload.path("ids");
ArrayNode ids = (ArrayNode) payload.path(IDS);
selectedHosts = getHosts(ids);
selectedDevices = getDevices(ids);
selectedIntents = intentFilter.findPathIntents(
......@@ -435,7 +469,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
if (haveSelectedIntents()) {
// Send a message to highlight all links of all monitored intents.
sendMessage(trafficMessage(new TrafficClass("primary", selectedIntents)));
sendMessage(trafficMessage(new TrafficClass(PRIMARY, selectedIntents)));
}
// TODO: Re-introduce once the client click vs hover gesture stuff is sorted out.
......@@ -548,7 +582,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
private synchronized void requestSummary(long sid) {
PropertyPanel pp = summmaryMessage(sid);
overlayCache.currentOverlay().modifySummary(pp);
ObjectNode json = JsonUtils.envelope("showSummary", sid, json(pp));
ObjectNode json = JsonUtils.envelope(SHOW_SUMMARY, sid, json(pp));
sendMessage(json);
}
......@@ -668,12 +702,12 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
startTrafficMonitoring();
// Get the set of selected hosts and their intents.
ArrayNode ids = (ArrayNode) payload.path("ids");
ArrayNode ids = (ArrayNode) payload.path(IDS);
Set<Host> hosts = new HashSet<>();
Set<Device> devices = getDevices(ids);
// If there is a hover node, include it in the hosts and find intents.
String hover = JsonUtils.string(payload, "hover");
String hover = JsonUtils.string(payload, HOVER);
if (!isNullOrEmpty(hover)) {
addHover(hosts, devices, hover);
}
......@@ -699,8 +733,8 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
secondary.removeAll(primary);
// Send a message to highlight all links of all monitored intents.
sendMessage(trafficMessage(new TrafficClass("primary", primary),
new TrafficClass("secondary", secondary)));
sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary),
new TrafficClass(SECONDARY, secondary)));
}
// Requests next or previous related intent.
......@@ -720,7 +754,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
// selected intent highlighted.
private void sendSelectedIntent() {
Intent selectedIntent = selectedIntents.get(currentIntentIndex);
log.info("Requested next intent {}", selectedIntent.id());
log.debug("Requested next intent {}", selectedIntent.id());
Set<Intent> primary = new HashSet<>();
primary.add(selectedIntent);
......@@ -729,8 +763,8 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
secondary.remove(selectedIntent);
// Send a message to highlight all links of the selected intent.
sendMessage(trafficMessage(new TrafficClass("primary", primary),
new TrafficClass("secondary", secondary)));
sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary),
new TrafficClass(SECONDARY, secondary)));
}
// Requests monitoring of traffic for the selected intent.
......@@ -740,13 +774,13 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
currentIntentIndex = 0;
}
Intent selectedIntent = selectedIntents.get(currentIntentIndex);
log.info("Requested traffic for selected {}", selectedIntent.id());
log.debug("Requested traffic for selected {}", selectedIntent.id());
Set<Intent> primary = new HashSet<>();
primary.add(selectedIntent);
// Send a message to highlight all links of the selected intent.
sendMessage(trafficMessage(new TrafficClass("primary", primary, true)));
sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary, true)));
}
}
......@@ -805,7 +839,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void event(MastershipEvent event) {
msgSender.execute(() -> {
sendAllInstances("updateInstance");
sendAllInstances(UPDATE_INSTANCE);
Device device = deviceService.getDevice(event.subject());
if (device != null) {
sendMessage(deviceMessage(new DeviceEvent(DEVICE_UPDATED, device)));
......
......@@ -440,57 +440,61 @@ public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler {
JsonUtils.node(payload, "memento"));
}
// Returns summary response.
// -----------------------------------------------------------------------
// Create models of the data to return, that overlays can adjust / augment
// Returns property panel model for summary response.
protected PropertyPanel summmaryMessage(long sid) {
Topology topology = topologyService.currentTopology();
PropertyPanel pp = new PropertyPanel("ONOS Summary", "node")
.add(new PropertyPanel.Prop("Devices", format(topology.deviceCount())))
.add(new PropertyPanel.Prop("Links", format(topology.linkCount())))
.add(new PropertyPanel.Prop("Hosts", format(hostService.getHostCount())))
.add(new PropertyPanel.Prop("Topology SCCs", format(topology.clusterCount())))
.add(new PropertyPanel.Prop("Devices", topology.deviceCount()))
.add(new PropertyPanel.Prop("Links", topology.linkCount()))
.add(new PropertyPanel.Prop("Hosts", hostService.getHostCount()))
.add(new PropertyPanel.Prop("Topology SCCs", topology.clusterCount()))
.add(new PropertyPanel.Separator())
.add(new PropertyPanel.Prop("Intents", format(intentService.getIntentCount())))
.add(new PropertyPanel.Prop("Tunnels", format(tunnelService.tunnelCount())))
.add(new PropertyPanel.Prop("Flows", format(flowService.getFlowRuleCount())))
.add(new PropertyPanel.Prop("Intents", intentService.getIntentCount()))
.add(new PropertyPanel.Prop("Tunnels", tunnelService.tunnelCount()))
.add(new PropertyPanel.Prop("Flows", flowService.getFlowRuleCount()))
.add(new PropertyPanel.Prop("Version", version));
return pp;
}
// Returns device details response.
protected ObjectNode deviceDetails(DeviceId deviceId, long sid) {
// Returns property panel model for device details response.
protected PropertyPanel deviceDetails(DeviceId deviceId, long sid) {
Device device = deviceService.getDevice(deviceId);
Annotations annot = device.annotations();
String name = annot.value(AnnotationKeys.NAME);
int portCount = deviceService.getPorts(deviceId).size();
int flowCount = getFlowCount(deviceId);
int tunnelCount = getTunnelCount(deviceId);
return JsonUtils.envelope("showDetails", sid,
json(isNullOrEmpty(name) ? deviceId.toString() : name,
device.type().toString().toLowerCase(),
new Prop("URI", deviceId.toString()),
new Prop("Vendor", device.manufacturer()),
new Prop("H/W Version", device.hwVersion()),
new Prop("S/W Version", device.swVersion()),
new Prop("Serial Number", device.serialNumber()),
new Prop("Protocol", annot.value(AnnotationKeys.PROTOCOL)),
new Separator(),
new Prop("Master", master(deviceId)),
new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)),
new Separator(),
new Prop("Ports", Integer.toString(portCount)),
new Prop("Flows", Integer.toString(flowCount)),
new Prop("Tunnels", Integer.toString(tunnelCount))
));
String title = isNullOrEmpty(name) ? deviceId.toString() : name;
String typeId = device.type().toString().toLowerCase();
PropertyPanel pp = new PropertyPanel(title, typeId)
.id(deviceId.toString())
.add(new PropertyPanel.Prop("URI", deviceId.toString()))
.add(new PropertyPanel.Prop("Vendor", device.manufacturer()))
.add(new PropertyPanel.Prop("H/W Version", device.hwVersion()))
.add(new PropertyPanel.Prop("S/W Version", device.swVersion()))
.add(new PropertyPanel.Prop("Serial Number", device.serialNumber()))
.add(new PropertyPanel.Prop("Protocol", annot.value(AnnotationKeys.PROTOCOL)))
.add(new PropertyPanel.Separator())
.add(new PropertyPanel.Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)))
.add(new PropertyPanel.Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)))
.add(new PropertyPanel.Separator())
.add(new PropertyPanel.Prop("Ports", portCount))
.add(new PropertyPanel.Prop("Flows", flowCount))
.add(new PropertyPanel.Prop("Tunnels", tunnelCount));
return pp;
}
protected int getFlowCount(DeviceId deviceId) {
int count = 0;
Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
while (it.hasNext()) {
for (FlowEntry flowEntry : flowService.getFlowEntries(deviceId)) {
count++;
it.next();
}
return count;
}
......@@ -503,8 +507,8 @@ public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler {
OpticalTunnelEndPoint dst = (OpticalTunnelEndPoint) tunnel.dst();
DeviceId srcDevice = (DeviceId) src.elementId().get();
DeviceId dstDevice = (DeviceId) dst.elementId().get();
if (srcDevice.toString().equals(deviceId.toString())
|| dstDevice.toString().equals(deviceId.toString())) {
if (srcDevice.toString().equals(deviceId.toString()) ||
dstDevice.toString().equals(deviceId.toString())) {
count++;
}
}
......@@ -516,9 +520,8 @@ public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler {
List<FlowEntry> entries = new ArrayList<>();
Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId));
Set<Host> hosts = hostService.getConnectedHosts(deviceId);
Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
while (it.hasNext()) {
entries.add(it.next());
for (FlowEntry flowEntry : flowService.getFlowEntries(deviceId)) {
entries.add(flowEntry);
}
// Add all edge links to the set
......@@ -555,24 +558,30 @@ public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler {
// Returns host details response.
protected ObjectNode hostDetails(HostId hostId, long sid) {
protected PropertyPanel hostDetails(HostId hostId, long sid) {
Host host = hostService.getHost(hostId);
Annotations annot = host.annotations();
String type = annot.value(AnnotationKeys.TYPE);
String name = annot.value(AnnotationKeys.NAME);
String vlan = host.vlan().toString();
return JsonUtils.envelope("showDetails", sid,
json(isNullOrEmpty(name) ? hostId.toString() : name,
isNullOrEmpty(type) ? "endstation" : type,
new Prop("MAC", host.mac().toString()),
new Prop("IP", host.ipAddresses().toString().replaceAll("[\\[\\]]", "")),
new Prop("VLAN", vlan.equals("-1") ? "none" : vlan),
new Separator(),
new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE))));
String title = isNullOrEmpty(name) ? hostId.toString() : name;
String typeId = isNullOrEmpty(type) ? "endstation" : type;
PropertyPanel pp = new PropertyPanel(title, typeId)
.id(hostId.toString())
.add(new PropertyPanel.Prop("MAC", host.mac().toString()))
.add(new PropertyPanel.Prop("IP", host.ipAddresses().toString().replaceAll("[\\[\\]]", "")))
.add(new PropertyPanel.Prop("VLAN", vlan.equals("-1") ? "none" : vlan))
.add(new PropertyPanel.Separator())
.add(new PropertyPanel.Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)))
.add(new PropertyPanel.Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)));
return pp;
}
// TODO: migrate to Traffic overlay
// Produces JSON message to trigger flow traffic overview visualization
protected ObjectNode trafficSummaryMessage(StatsType type) {
ObjectNode payload = objectNode();
......@@ -827,21 +836,19 @@ public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler {
return format.format(value) + " " + unit;
}
// Formats the given number into a string.
private String format(Number number) {
DecimalFormat format = new DecimalFormat("#,###");
return format.format(number);
}
// Produces compact string representation of a link.
private static String compactLinkString(Link link) {
return String.format(COMPACT, link.src().elementId(), link.src().port(),
link.dst().elementId(), link.dst().port());
}
// translates the property panel into JSON, for returning to the client
protected ObjectNode json(PropertyPanel pp) {
ObjectNode result = objectNode()
.put("title", pp.title()).put("type", pp.typeId());
.put("title", pp.title())
.put("type", pp.typeId())
.put("id", pp.id());
ObjectNode pnode = objectNode();
ArrayNode porder = arrayNode();
for (PropertyPanel.Prop p : pp.properties()) {
......
......@@ -219,6 +219,11 @@
// === -----------------------------------------------------
// Functions for populating the detail panel
var isDevice = {
switch: 1,
roadm: 1
};
function displaySingle(data) {
detail.setup();
......@@ -228,16 +233,21 @@
title = detail.appendHeader('h2')
.classed('clickable', true),
table = detail.appendBody('table'),
tbody = table.append('tbody');
tbody = table.append('tbody'),
navFn;
gs.addGlyph(svg, (data.type || 'unknown'), 40);
title.text(data.id);
svg.on('click', function () {
ns.navTo(devPath, { devId: data.id });
});
title.on('click', function () {
ns.navTo(devPath, { devId: data.id });
});
title.text(data.title);
// only add navigation when displaying a device
if (isDevice[data.type]) {
navFn = function () {
ns.navTo(devPath, { devId: data.id });
};
svg.on('click', navFn);
title.on('click', navFn);
}
listProps(tbody, data);
addBtnFooter();
......