Thomas Vachuska

ONOS-293 Added summary pane and related keyboard shortcuts; also tweaked key hel…

…p sizes and dropped instances toggle from mast. Fixed ONOS-295 bug.

Change-Id: I694901957451cf88df06e6fca3a8d71de144f68e
......@@ -43,10 +43,10 @@ public class SummaryCommand extends AbstractShellCommand {
.put("node", get(ClusterService.class).getLocalNode().ip().toString())
.put("version", get(CoreService.class).version().toString())
.put("nodes", get(ClusterService.class).getNodes().size())
.put("devices", get(DeviceService.class).getDeviceCount())
.put("links", get(LinkService.class).getLinkCount())
.put("devices", topology.deviceCount())
.put("links", topology.linkCount())
.put("hosts", get(HostService.class).getHostCount())
.put("clusters", topologyService.getClusters(topology).size())
.put("clusters", topology.clusterCount())
.put("paths", topology.pathCount())
.put("flows", get(FlowRuleService.class).getFlowRuleCount())
.put("intents", get(IntentService.class).getIntentCount()));
......
......@@ -4,7 +4,7 @@
# -----------------------------------------------------------------------------
#export JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-7-openjdk-amd64/}
export JAVA_OPTS="${JAVA_OPTS:--Xms256M -Xmx2048M}"
export JAVA_OPTS="${JAVA_OPTS:--Xms256m -Xmx2048m}"
cd /opt/onos
/opt/onos/apache-karaf-$KARAF_VERSION/bin/karaf "$@"
......
......@@ -23,6 +23,7 @@ import org.onlab.onos.cluster.ClusterEvent;
import org.onlab.onos.cluster.ClusterService;
import org.onlab.onos.cluster.ControllerNode;
import org.onlab.onos.cluster.NodeId;
import org.onlab.onos.core.CoreService;
import org.onlab.onos.mastership.MastershipService;
import org.onlab.onos.net.Annotated;
import org.onlab.onos.net.Annotations;
......@@ -56,6 +57,8 @@ import org.onlab.onos.net.link.LinkService;
import org.onlab.onos.net.provider.ProviderId;
import org.onlab.onos.net.statistic.Load;
import org.onlab.onos.net.statistic.StatisticService;
import org.onlab.onos.net.topology.Topology;
import org.onlab.onos.net.topology.TopologyService;
import org.onlab.osgi.ServiceDirectory;
import org.onlab.packet.IpAddress;
import org.slf4j.Logger;
......@@ -117,8 +120,10 @@ public abstract class TopologyViewMessages {
protected final IntentService intentService;
protected final FlowRuleService flowService;
protected final StatisticService statService;
protected final TopologyService topologyService;
protected final ObjectMapper mapper = new ObjectMapper();
private final String version;
// TODO: extract into an external & durable state; good enough for now and demo
private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
......@@ -138,6 +143,9 @@ public abstract class TopologyViewMessages {
intentService = directory.get(IntentService.class);
flowService = directory.get(FlowRuleService.class);
statService = directory.get(StatisticService.class);
topologyService = directory.get(TopologyService.class);
version = directory.get(CoreService.class).version().toString();
}
// Retrieves the payload from the specified event.
......@@ -419,6 +427,22 @@ public abstract class TopologyViewMessages {
metaUi.put(string(payload, "id"), (ObjectNode) payload.path("memento"));
}
// Returns summary response.
protected ObjectNode summmaryMessage(long sid) {
Topology topology = topologyService.currentTopology();
return envelope("showSummary", sid,
json("ONOS Summary", "node",
new Prop("Devices", format(topology.deviceCount())),
new Prop("Links", format(topology.linkCount())),
new Prop("Hosts", format(hostService.getHostCount())),
new Prop("Topology SCCs", format(topology.clusterCount())),
new Prop("Paths", format(topology.pathCount())),
new Separator(),
new Prop("Intents", format(intentService.getIntentCount())),
new Prop("Flows", format(flowService.getFlowRuleCount())),
new Prop("Version", version.replace(".SNAPSHOT", "*"))));
}
// Returns device details response.
protected ObjectNode deviceDetails(DeviceId deviceId, long sid) {
Device device = deviceService.getDevice(deviceId);
......@@ -435,12 +459,12 @@ public abstract class TopologyViewMessages {
new Prop("S/W Version", device.swVersion()),
new Prop("Serial Number", device.serialNumber()),
new Separator(),
new Prop("Master", master(deviceId)),
new Prop("Latitude", annot.value("latitude")),
new Prop("Longitude", annot.value("longitude")),
new Prop("Ports", Integer.toString(portCount)),
new Prop("Flows", Integer.toString(flowCount)),
new Separator(),
new Prop("Master", master(deviceId))));
new Prop("Ports", Integer.toString(portCount)),
new Prop("Flows", Integer.toString(flowCount))));
}
protected int getFlowCount(DeviceId deviceId) {
......@@ -641,6 +665,12 @@ public abstract class TopologyViewMessages {
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);
}
private boolean isInfrastructureEgress(Link link) {
return link.src().elementId() instanceof DeviceId;
}
......
......@@ -42,7 +42,11 @@ import org.onlab.onos.net.link.LinkListener;
import org.onlab.osgi.ServiceDirectory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
......@@ -70,8 +74,17 @@ public class TopologyViewWebSocket
private static final String APP_ID = "org.onlab.onos.gui";
private static final long SUMMARY_FREQUENCY_SEC = 2000;
private static final long TRAFFIC_FREQUENCY_SEC = 1000;
private static final Comparator<? super ControllerNode> NODE_COMPARATOR =
new Comparator<ControllerNode>() {
@Override
public int compare(ControllerNode o1, ControllerNode o2) {
return o1.id().toString().compareTo(o2.id().toString());
}
};
private final ApplicationId appId;
private Connection connection;
......@@ -83,10 +96,14 @@ public class TopologyViewWebSocket
private final HostListener hostListener = new InternalHostListener();
private final IntentListener intentListener = new InternalIntentListener();
// Intents that are being monitored for the GUI
private ObjectNode monitorRequest;
private final Timer timer = new Timer("intent-traffic-monitor");
private final TimerTask timerTask = new IntentTrafficMonitor();
// Timers and objects being monitored
private final Timer timer = new Timer("topology-view");
private TimerTask trafficTask;
private ObjectNode trafficEvent;
private TimerTask summaryTask;
private ObjectNode summaryEvent;
private long lastActive = System.currentTimeMillis();
private boolean listenersRemoved = false;
......@@ -140,7 +157,6 @@ public class TopologyViewWebSocket
this.connection = connection;
this.control = (FrameConnection) connection;
addListeners();
timer.schedule(timerTask, TRAFFIC_FREQUENCY_SEC, TRAFFIC_FREQUENCY_SEC);
sendAllInstances();
sendAllDevices();
......@@ -181,6 +197,7 @@ public class TopologyViewWebSocket
updateMetaUi(event);
} else if (type.equals("addHostIntent")) {
createHostIntent(event);
} else if (type.equals("requestTraffic")) {
requestTraffic(event);
} else if (type.equals("requestAllTraffic")) {
......@@ -189,6 +206,11 @@ public class TopologyViewWebSocket
requestDeviceLinkFlows(event);
} else if (type.equals("cancelTraffic")) {
cancelTraffic(event);
} else if (type.equals("requestSummary")) {
requestSummary(event);
} else if (type.equals("cancelSummary")) {
cancelSummary(event);
}
}
......@@ -205,7 +227,9 @@ public class TopologyViewWebSocket
// Sends all controller nodes to the client as node-added messages.
private void sendAllInstances() {
for (ControllerNode node : clusterService.getNodes()) {
List<ControllerNode> nodes = new ArrayList<>(clusterService.getNodes());
Collections.sort(nodes, NODE_COMPARATOR);
for (ControllerNode node : nodes) {
sendMessage(instanceMessage(new ClusterEvent(INSTANCE_ADDED, node)));
}
}
......@@ -255,22 +279,37 @@ public class TopologyViewWebSocket
HostToHostIntent hostIntent = new HostToHostIntent(appId, one, two,
DefaultTrafficSelector.builder().build(),
DefaultTrafficTreatment.builder().build());
monitorRequest = event;
trafficEvent = event;
intentService.submit(hostIntent);
}
private synchronized long startMonitoring(ObjectNode event) {
if (trafficTask == null) {
trafficEvent = event;
trafficTask = new TrafficMonitor();
timer.schedule(trafficTask, TRAFFIC_FREQUENCY_SEC, TRAFFIC_FREQUENCY_SEC);
}
return number(event, "sid");
}
private synchronized void stopMonitoring() {
if (trafficTask != null) {
trafficTask.cancel();
trafficTask = null;
trafficEvent = null;
}
}
// Subscribes for host traffic messages.
private synchronized void requestAllTraffic(ObjectNode event) {
ObjectNode payload = payload(event);
long sid = number(event, "sid");
monitorRequest = event;
long sid = startMonitoring(event);
sendMessage(trafficSummaryMessage(sid));
}
private void requestDeviceLinkFlows(ObjectNode event) {
ObjectNode payload = payload(event);
long sid = number(event, "sid");
monitorRequest = event;
long sid = startMonitoring(event);
// Get the set of selected hosts and their intents.
ArrayNode ids = (ArrayNode) payload.path("ids");
......@@ -294,8 +333,7 @@ public class TopologyViewWebSocket
return;
}
long sid = number(event, "sid");
monitorRequest = event;
long sid = startMonitoring(event);
// Get the set of selected hosts and their intents.
ArrayNode ids = (ArrayNode) payload.path("ids");
......@@ -325,9 +363,30 @@ public class TopologyViewWebSocket
// Cancels sending traffic messages.
private void cancelTraffic(ObjectNode event) {
sendMessage(trafficMessage(number(event, "sid")));
monitorRequest = null;
stopMonitoring();
}
// Subscribes for summary messages.
private synchronized void requestSummary(ObjectNode event) {
if (summaryTask == null) {
summaryEvent = event;
summaryTask = new SummaryMonitor();
timer.schedule(summaryTask, SUMMARY_FREQUENCY_SEC, SUMMARY_FREQUENCY_SEC);
}
sendMessage(summmaryMessage(number(event, "sid")));
}
// Cancels sending summary messages.
private synchronized void cancelSummary(ObjectNode event) {
if (summaryTask != null) {
summaryTask.cancel();
summaryTask = null;
summaryEvent = null;
}
}
// Adds all internal listeners.
private void addListeners() {
clusterService.addListener(clusterListener);
......@@ -385,26 +444,36 @@ public class TopologyViewWebSocket
private class InternalIntentListener implements IntentListener {
@Override
public void event(IntentEvent event) {
if (monitorRequest != null) {
requestTraffic(monitorRequest);
if (trafficEvent != null) {
requestTraffic(trafficEvent);
}
}
}
private class IntentTrafficMonitor extends TimerTask {
private class TrafficMonitor extends TimerTask {
@Override
public void run() {
if (monitorRequest != null) {
String type = string(monitorRequest, "event", "unknown");
if (trafficEvent != null) {
String type = string(trafficEvent, "event", "unknown");
if (type.equals("requestAllTraffic")) {
requestAllTraffic(monitorRequest);
requestAllTraffic(trafficEvent);
} else if (type.equals("requestDeviceLinkFlows")) {
requestDeviceLinkFlows(monitorRequest);
requestDeviceLinkFlows(trafficEvent);
} else {
requestTraffic(monitorRequest);
requestTraffic(trafficEvent);
}
}
}
}
private class SummaryMonitor extends TimerTask {
@Override
public void run() {
if (summaryEvent != null) {
requestSummary(summaryEvent);
}
}
}
}
......
......@@ -23,7 +23,7 @@
#feedback svg {
position: absolute;
bottom: 0;
opacity: 0.5;
opacity: 0.8;
}
#feedback svg g.feedbackItem {
......@@ -31,14 +31,11 @@
}
#feedback svg g.feedbackItem rect {
fill: #888;
stroke: #666;
stroke-width: 3;
opacity: 0.5
fill: #ccc;
}
#feedback svg g.feedbackItem text {
fill: #000;
fill: #333;
stroke: none;
text-anchor: middle;
alignment-baseline: middle;
......
......@@ -33,7 +33,7 @@
var w = '100%',
h = 200,
fade = 200,
showFor = 500,
showFor = 1200,
vb = '-200 -' + (h/2) + ' 400 ' + h,
xpad = 20,
ypad = 10;
......
......@@ -24,8 +24,8 @@
position: absolute;
z-index: 100;
display: block;
top: 10%;
width: 280px;
top: 64px;
width: 260px;
right: -300px;
opacity: 0;
background-color: rgba(255,255,255,0.8);
......
......@@ -31,10 +31,10 @@
}
#keymap svg text.title {
font-size: 12pt;
font-size: 10pt;
font-style: italic;
text-anchor: middle;
fill: #444;
fill: #999;
}
#keymap svg g.keyItem {
......@@ -47,17 +47,17 @@
}
#keymap svg text {
font-size: 10pt;
font-size: 7pt;
alignment-baseline: middle;
}
#keymap svg text.key {
font-size: 10pt;
fill: #8aa;
font-size: 7pt;
fill: #add;
}
#keymap svg text.desc {
font-size: 10pt;
fill: #888;
font-size: 7pt;
fill: #aaa;
}
......
......@@ -35,9 +35,9 @@
fade = 500,
vb = '-220 -220 440 440',
paneW = 400,
paneH = 340,
paneH = 280,
offy = 65,
dy = 20,
dy = 14,
offKey = 40,
offDesc = offKey + 50,
lineW = paneW - (2*offKey);
......
......@@ -763,7 +763,8 @@
var pos = position || 'TR',
cfg = fpConfig[pos],
el,
fp;
fp,
on = false;
if (fpanels[id]) {
buildError('Float panel with id "' + id + '" already exists.');
......@@ -792,15 +793,20 @@
id: id,
el: el,
pos: pos,
isVisible: function () {
return on;
},
show: function () {
console.log('show pane: ' + id);
on = true;
el.transition().duration(750)
.style(cfg.side, pxShow())
.style('opacity', 1);
},
hide: function () {
console.log('hide pane: ' + id);
on = false;
el.transition().duration(750)
.style(cfg.side, pxHide())
.style('opacity', 0);
......
......@@ -177,10 +177,66 @@ svg .node.host circle {
font-size: 9pt;
}
/* Fly-in summary pane */
#topo-summary {
/* gets base CSS from .fpanel in floatPanel.css */
top: 64px;
}
#topo-summary svg {
display: inline-block;
width: 42px;
height: 42px;
}
#topo-summary svg .glyphIcon {
fill: black;
stroke: none;
fill-rule: evenodd;
}
#topo-summary h2 {
position: absolute;
margin: 0px 4px;
top: 20px;
left: 50px;
color: black;
}
#topo-summary h3 {
margin: 0px 4px;
top: 20px;
left: 50px;
color: black;
}
#topo-summary p, table {
margin: 4px 4px;
}
#topo-summary td.label {
font-style: italic;
color: #777;
padding-right: 12px;
}
#topo-summary td.value {
}
#topo-summary hr {
height: 1px;
color: #ccc;
background-color: #ccc;
border: 0;
}
/* Fly-in details pane */
#topo-detail {
/* gets base CSS from .fpanel in floatPanel.css */
/* gets base CSS from .fpanel in floatPanel.css */
top: 320px;
}
#topo-detail svg {
......
......@@ -141,6 +141,8 @@
S: injectStartupEvents,
space: injectTestEvent,
O: [toggleSummary, 'Toggle ONOS summary pane'],
I: [toggleInstances, 'Toggle ONOS instances pane'],
B: [toggleBg, 'Toggle background image'],
L: [cycleLabels, 'Cycle Device labels'],
P: togglePorts,
......@@ -182,6 +184,7 @@
selections = {},
selectOrder = [],
hovered = null,
summaryPane,
detailPane,
antTimer = null,
onosInstances = {},
......@@ -329,7 +332,7 @@
if (hoverMode === hoverModes.length) {
hoverMode = 0;
}
view.flash('Hover Mode: ' + hoverModes[hoverMode]);
view.flash('Mode: ' + hoverModes[hoverMode]);
}
function togglePorts(view) {
......@@ -347,8 +350,12 @@
function handleEscape(view) {
if (oiShowMaster) {
cancelAffinity();
} else {
} else if (detailPane.isVisible()) {
deselectAll();
} else if (oiBox.isVisible()) {
oiBox.hide();
} else if (summaryPane.isVisible()) {
cancelSummary();
}
}
......@@ -585,6 +592,7 @@
removeHost: removeHost,
showDetails: showDetails,
showSummary: showSummary,
showTraffic: showTraffic
};
......@@ -737,6 +745,12 @@
}
}
function showSummary(data) {
evTrace(data);
populateSummary(data.payload);
summaryPane.show();
}
function showDetails(data) {
evTrace(data);
populateDetails(data.payload);
......@@ -824,6 +838,33 @@
return true;
}
function toggleInstances() {
if (!oiBox.isVisible()) {
oiBox.show();
} else {
oiBox.hide();
}
}
function toggleSummary() {
if (!summaryPane.isVisible()) {
requestSummary();
} else {
cancelSummary();
}
}
// request overall summary data
function requestSummary() {
sendMessage('requestSummary', {});
}
function cancelSummary() {
sendMessage('cancelSummary', {});
summaryPane.hide();
}
// request details for the selected element
// invoked from selection of a single node.
function requestDetails() {
......@@ -845,16 +886,20 @@
}
function showTrafficAction() {
// force intents hover mode
cancelTraffic();
hoverMode = 1;
showSelectTraffic();
network.view.flash('Related Traffic');
}
function cancelTraffic() {
sendMessage('cancelTraffic', {});
}
function showSelectTraffic() {
// if nothing is hovered over, and nothing selected, send cancel request
if (!hovered && nSel() === 0) {
sendMessage('cancelTraffic', {});
cancelTraffic();
return;
}
......@@ -870,12 +915,13 @@
}
function showAllTrafficAction() {
cancelTraffic();
sendMessage('requestAllTraffic', {});
network.view.flash('All Traffic');
}
function showDeviceLinkFlowsAction() {
// force intents hover mode
cancelTraffic();
hoverMode = 2;
showDeviceLinkFlows();
network.view.flash('Device Flows');
......@@ -884,7 +930,7 @@
function showDeviceLinkFlows() {
// if nothing is hovered over, and nothing selected, send cancel request
if (!hovered && nSel() === 0) {
sendMessage('cancelTraffic', {});
cancelTraffic();
return;
}
var hoverId = (flowsHover() && hovered && hovered.class === 'device') ?
......@@ -907,7 +953,6 @@
'xlink:href': iid,
width: dim,
height: dim
});
}
......@@ -940,6 +985,15 @@
});
var dim = 30;
appendGlyph(svg, 2, 2, 30, '#node');
svg.append('use')
.attr({
class: 'birdBadge',
transform: translate(8,10),
'xlink:href': '#bird',
width: 18,
height: 18,
fill: '#fff'
});
$('<div>').attr('class', 'onosTitle').text(d.id).appendTo(el);
......@@ -1720,6 +1774,8 @@
webSock.ws.onopen = function() {
noWebSock(false);
requestSummary();
oiBox.show();
};
webSock.ws.onmessage = function(m) {
......@@ -1881,12 +1937,17 @@
updateDetailPane();
}
// update the state of the sumary pane
function updateSummaryPane() {
}
// update the state of the detail pane, based on current selections
function updateDetailPane() {
var nSel = selectOrder.length;
if (!nSel) {
detailPane.hide();
showTrafficAction(); // sends cancelTraffic event
cancelTraffic();
} else if (nSel === 1) {
singleSelect();
} else {
......@@ -1936,6 +1997,40 @@
addMultiSelectActions();
}
// TODO: refactor to consolidate with populateDetails
function populateSummary(data) {
summaryPane.empty();
var svg = summaryPane.append('svg'),
iid = iconGlyphUrl(data);
var title = summaryPane.append('h2'),
table = summaryPane.append('table'),
tbody = table.append('tbody');
appendGlyph(svg, 0, 0, 40, iid);
svg.append('use')
.attr({
class: 'birdBadge',
transform: translate(8,12),
'xlink:href': '#bird',
width: 24,
height: 24,
fill: '#fff'
});
title.text('ONOS Summary');
data.propOrder.forEach(function(p) {
if (p === '-') {
addSep(tbody);
} else {
addProp(tbody, p, data.props[p]);
}
});
}
function populateDetails(data) {
detailPane.empty();
......@@ -2056,7 +2151,7 @@
// TODO: toggle button (and other widgets in the masthead) should be provided
// by the framework; not generated by the view.
var showInstances;
//var showInstances;
function addButtonBar(view) {
var bb = d3.select('#mast')
......@@ -2069,20 +2164,20 @@
.on('click', cb);
}
showInstances = mkTogBtn('Show Instances', toggleInst);
//showInstances = mkTogBtn('Show Instances', toggleInst);
}
function instShown() {
return showInstances.classed('active');
}
function toggleInst() {
showInstances.classed('active', !instShown());
if (instShown()) {
oiBox.show();
} else {
oiBox.hide();
}
}
//function instShown() {
// return showInstances.classed('active');
//}
//function toggleInst() {
// showInstances.classed('active', !instShown());
// if (instShown()) {
// oiBox.show();
// } else {
// oiBox.hide();
// }
//}
function panZoom() {
return false;
......@@ -2370,6 +2465,7 @@
resize: resize
});
summaryPane = onos.ui.addFloatingPanel('topo-summary');
detailPane = onos.ui.addFloatingPanel('topo-detail');
oiBox = onos.ui.addFloatingPanel('topo-oibox', 'TL');
......