Thomas Vachuska
Committed by Gerrit Code Review

Removed legacy GUI.

FIxed an NPE in the devices table.
Added cord-gui to obs.exclude

Change-Id: I18a53d2c44c6c97fb7f5d16b7446942a1a37ddca
Showing 34 changed files with 5 additions and 8932 deletions
......@@ -3,3 +3,4 @@
.*/build/conf/.*
.*/docs/.*
.*/openflow/drivers/.*
.*/cord-gui/.*
\ No newline at end of file
......
......@@ -121,6 +121,8 @@ public class DeviceViewMessageHandler extends UiMessageHandler {
boolean available = ds.isAvailable(id);
String iconId = available ? ICON_ID_ONLINE : ICON_ID_OFFLINE;
String protocol = dev.annotations().value(PROTOCOL);
row.cell(ID, id)
.cell(AVAILABLE, available)
.cell(AVAILABLE_IID, iconId)
......@@ -128,7 +130,7 @@ public class DeviceViewMessageHandler extends UiMessageHandler {
.cell(MFR, dev.manufacturer())
.cell(HW, dev.hwVersion())
.cell(SW, dev.swVersion())
.cell(PROTOCOL, dev.annotations().value(PROTOCOL))
.cell(PROTOCOL, protocol != null ? protocol : "")
.cell(NUM_PORTS, ds.getPorts(id).size())
.cell(MASTER_ID, ms.getMasterFor(id));
}
......
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui.impl;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.onlab.osgi.DefaultServiceDirectory;
import org.onlab.osgi.ServiceDirectory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
/**
* Web socket servlet capable of creating various sockets for the user interface.
*/
@Deprecated
public class GuiWebSocketServlet extends WebSocketServlet {
private static final long PING_DELAY_MS = 5000;
private ServiceDirectory directory = new DefaultServiceDirectory();
private final Set<TopologyViewWebSocket> sockets = new HashSet<>();
private final Timer timer = new Timer();
private final TimerTask pruner = new Pruner();
@Override
public void init() throws ServletException {
super.init();
timer.schedule(pruner, PING_DELAY_MS, PING_DELAY_MS);
}
@Override
public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
TopologyViewWebSocket socket = new TopologyViewWebSocket(directory);
synchronized (sockets) {
sockets.add(socket);
}
return socket;
}
// Task for pruning web-sockets that are idle.
private class Pruner extends TimerTask {
@Override
public void run() {
synchronized (sockets) {
Iterator<TopologyViewWebSocket> it = sockets.iterator();
while (it.hasNext()) {
TopologyViewWebSocket socket = it.next();
if (socket.isIdle()) {
it.remove();
socket.close();
}
}
}
}
}
}
......@@ -52,7 +52,7 @@ public class TopologyResource extends BaseResource {
ArrayNode devices = mapper.createArrayNode();
ArrayNode hosts = mapper.createArrayNode();
Map<String, ObjectNode> metaUi = TopologyViewMessages.getMetaUi();
Map<String, ObjectNode> metaUi = TopologyViewMessageHandler.getMetaUi();
for (String id : metaUi.keySet()) {
ObjectNode memento = metaUi.get(id);
if (id.charAt(17) == '/') {
......
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onlab.osgi.ServiceDirectory;
import org.onlab.packet.IpAddress;
import org.onosproject.cluster.ClusterEvent;
import org.onosproject.cluster.ClusterService;
import org.onosproject.cluster.ControllerNode;
import org.onosproject.cluster.NodeId;
import org.onosproject.core.CoreService;
import org.onosproject.mastership.MastershipService;
import org.onosproject.net.Annotated;
import org.onosproject.net.AnnotationKeys;
import org.onosproject.net.Annotations;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultEdgeLink;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.EdgeLink;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
import org.onosproject.net.HostLocation;
import org.onosproject.net.Link;
import org.onosproject.net.LinkKey;
import org.onosproject.net.PortNumber;
import org.onosproject.net.device.DeviceEvent;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.flow.FlowEntry;
import org.onosproject.net.flow.FlowRuleService;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.flow.instructions.Instruction;
import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
import org.onosproject.net.host.HostEvent;
import org.onosproject.net.host.HostService;
import org.onosproject.net.intent.Intent;
import org.onosproject.net.intent.IntentService;
import org.onosproject.net.intent.LinkCollectionIntent;
import org.onosproject.net.intent.OpticalConnectivityIntent;
import org.onosproject.net.intent.OpticalPathIntent;
import org.onosproject.net.intent.PathIntent;
import org.onosproject.net.link.LinkEvent;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.net.statistic.Load;
import org.onosproject.net.statistic.StatisticService;
import org.onosproject.net.topology.Topology;
import org.onosproject.net.topology.TopologyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_REMOVED;
import static org.onosproject.cluster.ControllerNode.State.ACTIVE;
import static org.onosproject.net.DeviceId.deviceId;
import static org.onosproject.net.HostId.hostId;
import static org.onosproject.net.LinkKey.linkKey;
import static org.onosproject.net.PortNumber.P0;
import static org.onosproject.net.PortNumber.portNumber;
import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_REMOVED;
import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED;
import static org.onosproject.net.host.HostEvent.Type.HOST_REMOVED;
import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
import static org.onosproject.net.link.LinkEvent.Type.LINK_REMOVED;
/**
* Facility for creating messages bound for the topology viewer.
*/
@Deprecated
public abstract class TopologyViewMessages {
protected static final Logger log = LoggerFactory.getLogger(TopologyViewMessages.class);
private static final ProviderId PID = new ProviderId("core", "org.onosproject.core", true);
private static final String COMPACT = "%s/%s-%s/%s";
private static final double KB = 1024;
private static final double MB = 1024 * KB;
private static final double GB = 1024 * MB;
private static final String GB_UNIT = "GB";
private static final String MB_UNIT = "MB";
private static final String KB_UNIT = "KB";
private static final String B_UNIT = "B";
protected final ServiceDirectory directory;
protected final ClusterService clusterService;
protected final DeviceService deviceService;
protected final LinkService linkService;
protected final HostService hostService;
protected final MastershipService mastershipService;
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<>();
/**
* Returns read-only view of the meta-ui information.
*
* @return map of id to meta-ui mementos
*/
static Map<String, ObjectNode> getMetaUi() {
return Collections.unmodifiableMap(metaUi);
}
/**
* Creates a messaging facility for creating messages for topology viewer.
*
* @param directory service directory
*/
protected TopologyViewMessages(ServiceDirectory directory) {
this.directory = checkNotNull(directory, "Directory cannot be null");
clusterService = directory.get(ClusterService.class);
deviceService = directory.get(DeviceService.class);
linkService = directory.get(LinkService.class);
hostService = directory.get(HostService.class);
mastershipService = directory.get(MastershipService.class);
intentService = directory.get(IntentService.class);
flowService = directory.get(FlowRuleService.class);
statService = directory.get(StatisticService.class);
topologyService = directory.get(TopologyService.class);
String ver = directory.get(CoreService.class).version().toString();
version = ver.replace(".SNAPSHOT", "*").replaceFirst("~.*$", "");
}
// Retrieves the payload from the specified event.
protected ObjectNode payload(ObjectNode event) {
return (ObjectNode) event.path("payload");
}
// Returns the specified node property as a number
protected long number(ObjectNode node, String name) {
return node.path(name).asLong();
}
// Returns the specified node property as a string.
protected String string(ObjectNode node, String name) {
return node.path(name).asText();
}
// Returns the specified node property as a string.
protected String string(ObjectNode node, String name, String defaultValue) {
return node.path(name).asText(defaultValue);
}
// Returns the specified set of IP addresses as a string.
private String ip(Set<IpAddress> ipAddresses) {
Iterator<IpAddress> it = ipAddresses.iterator();
return it.hasNext() ? it.next().toString() : "unknown";
}
// Produces JSON structure from annotations.
private JsonNode props(Annotations annotations) {
ObjectNode props = mapper.createObjectNode();
if (annotations != null) {
for (String key : annotations.keys()) {
props.put(key, annotations.value(key));
}
}
return props;
}
// Produces an informational log message event bound to the client.
protected ObjectNode info(long id, String message) {
return message("info", id, message);
}
// Produces a warning log message event bound to the client.
protected ObjectNode warning(long id, String message) {
return message("warning", id, message);
}
// Produces an error log message event bound to the client.
protected ObjectNode error(long id, String message) {
return message("error", id, message);
}
// Produces a log message event bound to the client.
private ObjectNode message(String severity, long id, String message) {
return envelope("message", id,
mapper.createObjectNode()
.put("severity", severity)
.put("message", message));
}
// Puts the payload into an envelope and returns it.
protected ObjectNode envelope(String type, long sid, ObjectNode payload) {
ObjectNode event = mapper.createObjectNode();
event.put("event", type);
if (sid > 0) {
event.put("sid", sid);
}
event.set("payload", payload);
return event;
}
// Produces a set of all hosts listed in the specified JSON array.
protected Set<Host> getHosts(ArrayNode array) {
Set<Host> hosts = new HashSet<>();
if (array != null) {
for (JsonNode node : array) {
try {
addHost(hosts, hostId(node.asText()));
} catch (IllegalArgumentException e) {
log.debug("Skipping ID {}", node.asText());
}
}
}
return hosts;
}
// Adds the specified host to the set of hosts.
private void addHost(Set<Host> hosts, HostId hostId) {
Host host = hostService.getHost(hostId);
if (host != null) {
hosts.add(host);
}
}
// Produces a set of all devices listed in the specified JSON array.
protected Set<Device> getDevices(ArrayNode array) {
Set<Device> devices = new HashSet<>();
if (array != null) {
for (JsonNode node : array) {
try {
addDevice(devices, deviceId(node.asText()));
} catch (IllegalArgumentException e) {
log.debug("Skipping ID {}", node.asText());
}
}
}
return devices;
}
private void addDevice(Set<Device> devices, DeviceId deviceId) {
Device device = deviceService.getDevice(deviceId);
if (device != null) {
devices.add(device);
}
}
protected void addHover(Set<Host> hosts, Set<Device> devices, String hover) {
try {
addHost(hosts, hostId(hover));
} catch (IllegalArgumentException e) {
try {
addDevice(devices, deviceId(hover));
} catch (IllegalArgumentException ne) {
log.debug("Skipping ID {}", hover);
}
}
}
// Produces a cluster instance message to the client.
protected ObjectNode instanceMessage(ClusterEvent event, String messageType) {
ControllerNode node = event.subject();
int switchCount = mastershipService.getDevicesOf(node.id()).size();
ObjectNode payload = mapper.createObjectNode()
.put("id", node.id().toString())
.put("ip", node.ip().toString())
.put("online", clusterService.getState(node.id()) == ACTIVE)
.put("uiAttached", event.subject().equals(clusterService.getLocalNode()))
.put("switches", switchCount);
ArrayNode labels = mapper.createArrayNode();
labels.add(node.id().toString());
labels.add(node.ip().toString());
// Add labels, props and stuff the payload into envelope.
payload.set("labels", labels);
addMetaUi(node.id().toString(), payload);
String type = messageType != null ? messageType :
((event.type() == INSTANCE_ADDED) ? "addInstance" :
((event.type() == INSTANCE_REMOVED ? "removeInstance" :
"addInstance")));
return envelope(type, 0, payload);
}
// Produces a device event message to the client.
protected ObjectNode deviceMessage(DeviceEvent event) {
Device device = event.subject();
ObjectNode payload = mapper.createObjectNode()
.put("id", device.id().toString())
.put("type", device.type().toString().toLowerCase())
.put("online", deviceService.isAvailable(device.id()))
.put("master", master(device.id()));
// Generate labels: id, chassis id, no-label, optional-name
String name = device.annotations().value(AnnotationKeys.NAME);
ArrayNode labels = mapper.createArrayNode();
labels.add("");
labels.add(isNullOrEmpty(name) ? device.id().toString() : name);
labels.add(device.id().toString());
// Add labels, props and stuff the payload into envelope.
payload.set("labels", labels);
payload.set("props", props(device.annotations()));
addGeoLocation(device, payload);
addMetaUi(device.id().toString(), payload);
String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
return envelope(type, 0, payload);
}
// Produces a link event message to the client.
protected ObjectNode linkMessage(LinkEvent event) {
Link link = event.subject();
ObjectNode payload = mapper.createObjectNode()
.put("id", compactLinkString(link))
.put("type", link.type().toString().toLowerCase())
.put("online", link.state() == Link.State.ACTIVE)
.put("linkWidth", 1.2)
.put("src", link.src().deviceId().toString())
.put("srcPort", link.src().port().toString())
.put("dst", link.dst().deviceId().toString())
.put("dstPort", link.dst().port().toString());
String type = (event.type() == LINK_ADDED) ? "addLink" :
((event.type() == LINK_REMOVED) ? "removeLink" : "updateLink");
return envelope(type, 0, payload);
}
// Produces a host event message to the client.
protected ObjectNode hostMessage(HostEvent event) {
Host host = event.subject();
String hostType = host.annotations().value(AnnotationKeys.TYPE);
ObjectNode payload = mapper.createObjectNode()
.put("id", host.id().toString())
.put("type", isNullOrEmpty(hostType) ? "endstation" : hostType)
.put("ingress", compactLinkString(edgeLink(host, true)))
.put("egress", compactLinkString(edgeLink(host, false)));
payload.set("cp", hostConnect(mapper, host.location()));
payload.set("labels", labels(mapper, ip(host.ipAddresses()),
host.mac().toString()));
payload.set("props", props(host.annotations()));
addGeoLocation(host, payload);
addMetaUi(host.id().toString(), payload);
String type = (event.type() == HOST_ADDED) ? "addHost" :
((event.type() == HOST_REMOVED) ? "removeHost" : "updateHost");
return envelope(type, 0, payload);
}
// Encodes the specified host location into a JSON object.
private ObjectNode hostConnect(ObjectMapper mapper, HostLocation location) {
return mapper.createObjectNode()
.put("device", location.deviceId().toString())
.put("port", location.port().toLong());
}
// Encodes the specified list of labels a JSON array.
private ArrayNode labels(ObjectMapper mapper, String... labels) {
ArrayNode json = mapper.createArrayNode();
for (String label : labels) {
json.add(label);
}
return json;
}
// Returns the name of the master node for the specified device id.
private String master(DeviceId deviceId) {
NodeId master = mastershipService.getMasterFor(deviceId);
return master != null ? master.toString() : "";
}
// Generates an edge link from the specified host location.
private EdgeLink edgeLink(Host host, boolean ingress) {
return new DefaultEdgeLink(PID, new ConnectPoint(host.id(), portNumber(0)),
host.location(), ingress);
}
// Adds meta UI information for the specified object.
private void addMetaUi(String id, ObjectNode payload) {
ObjectNode meta = metaUi.get(id);
if (meta != null) {
payload.set("metaUi", meta);
}
}
// Adds a geo location JSON to the specified payload object.
private void addGeoLocation(Annotated annotated, ObjectNode payload) {
Annotations annotations = annotated.annotations();
if (annotations == null) {
return;
}
String slat = annotations.value(AnnotationKeys.LATITUDE);
String slng = annotations.value(AnnotationKeys.LONGITUDE);
try {
if (slat != null && slng != null && !slat.isEmpty() && !slng.isEmpty()) {
double lat = Double.parseDouble(slat);
double lng = Double.parseDouble(slng);
ObjectNode loc = mapper.createObjectNode()
.put("type", "latlng").put("lat", lat).put("lng", lng);
payload.set("location", loc);
}
} catch (NumberFormatException e) {
log.warn("Invalid geo data latitude={}; longiture={}", slat, slng);
}
}
// Updates meta UI information for the specified object.
protected void updateMetaUi(ObjectNode event) {
ObjectNode payload = payload(event);
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 Separator(),
new Prop("Intents", format(intentService.getIntentCount())),
new Prop("Flows", format(flowService.getFlowRuleCount())),
new Prop("Version", version)));
}
// Returns device details response.
protected ObjectNode 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);
return 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))));
}
protected int getFlowCount(DeviceId deviceId) {
int count = 0;
Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
while (it.hasNext()) {
count++;
it.next();
}
return count;
}
// Counts all entries that egress on the given device links.
protected Map<Link, Integer> getFlowCounts(DeviceId deviceId) {
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());
}
// Add all edge links to the set
if (hosts != null) {
for (Host host : hosts) {
links.add(new DefaultEdgeLink(host.providerId(),
new ConnectPoint(host.id(), P0),
host.location(), false));
}
}
Map<Link, Integer> counts = new HashMap<>();
for (Link link : links) {
counts.put(link, getEgressFlows(link, entries));
}
return counts;
}
// Counts all entries that egress on the link source port.
private Integer getEgressFlows(Link link, List<FlowEntry> entries) {
int count = 0;
PortNumber out = link.src().port();
for (FlowEntry entry : entries) {
TrafficTreatment treatment = entry.treatment();
for (Instruction instruction : treatment.allInstructions()) {
if (instruction.type() == Instruction.Type.OUTPUT &&
((OutputInstruction) instruction).port().equals(out)) {
count++;
}
}
}
return count;
}
// Returns host details response.
protected ObjectNode 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 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))));
}
// Produces JSON message to trigger traffic overview visualization
protected ObjectNode trafficSummaryMessage(long sid) {
ObjectNode payload = mapper.createObjectNode();
ArrayNode paths = mapper.createArrayNode();
payload.set("paths", paths);
ObjectNode pathNodeN = mapper.createObjectNode();
ArrayNode linksNodeN = mapper.createArrayNode();
ArrayNode labelsN = mapper.createArrayNode();
pathNodeN.put("class", "plain").put("traffic", false);
pathNodeN.set("links", linksNodeN);
pathNodeN.set("labels", labelsN);
paths.add(pathNodeN);
ObjectNode pathNodeT = mapper.createObjectNode();
ArrayNode linksNodeT = mapper.createArrayNode();
ArrayNode labelsT = mapper.createArrayNode();
pathNodeT.put("class", "secondary").put("traffic", true);
pathNodeT.set("links", linksNodeT);
pathNodeT.set("labels", labelsT);
paths.add(pathNodeT);
for (BiLink link : consolidateLinks(linkService.getLinks())) {
boolean bi = link.two != null;
if (isInfrastructureEgress(link.one) ||
(bi && isInfrastructureEgress(link.two))) {
link.addLoad(statService.load(link.one));
link.addLoad(bi ? statService.load(link.two) : null);
if (link.hasTraffic) {
linksNodeT.add(compactLinkString(link.one));
labelsT.add(formatBytes(link.bytes));
} else {
linksNodeN.add(compactLinkString(link.one));
labelsN.add("");
}
}
}
return envelope("showTraffic", sid, payload);
}
private Collection<BiLink> consolidateLinks(Iterable<Link> links) {
Map<LinkKey, BiLink> biLinks = new HashMap<>();
for (Link link : links) {
addLink(biLinks, link);
}
return biLinks.values();
}
// Produces JSON message to trigger flow overview visualization
protected ObjectNode flowSummaryMessage(long sid, Set<Device> devices) {
ObjectNode payload = mapper.createObjectNode();
ArrayNode paths = mapper.createArrayNode();
payload.set("paths", paths);
for (Device device : devices) {
Map<Link, Integer> counts = getFlowCounts(device.id());
for (Link link : counts.keySet()) {
addLinkFlows(link, paths, counts.get(link));
}
}
return envelope("showTraffic", sid, payload);
}
private void addLinkFlows(Link link, ArrayNode paths, Integer count) {
ObjectNode pathNode = mapper.createObjectNode();
ArrayNode linksNode = mapper.createArrayNode();
ArrayNode labels = mapper.createArrayNode();
boolean noFlows = count == null || count == 0;
pathNode.put("class", noFlows ? "secondary" : "primary");
pathNode.put("traffic", false);
pathNode.set("links", linksNode.add(compactLinkString(link)));
pathNode.set("labels", labels.add(noFlows ? "" : (count.toString() +
(count == 1 ? " flow" : " flows"))));
paths.add(pathNode);
}
// Produces JSON message to trigger traffic visualization
protected ObjectNode trafficMessage(long sid, TrafficClass... trafficClasses) {
ObjectNode payload = mapper.createObjectNode();
ArrayNode paths = mapper.createArrayNode();
payload.set("paths", paths);
// Classify links based on their traffic traffic first...
Map<LinkKey, BiLink> biLinks = classifyLinkTraffic(trafficClasses);
// Then separate the links into their respective classes and send them out.
Map<String, ObjectNode> pathNodes = new HashMap<>();
for (BiLink biLink : biLinks.values()) {
boolean hasTraffic = biLink.hasTraffic;
String tc = (biLink.classes + (hasTraffic ? " animated" : "")).trim();
ObjectNode pathNode = pathNodes.get(tc);
if (pathNode == null) {
pathNode = mapper.createObjectNode()
.put("class", tc).put("traffic", hasTraffic);
pathNode.set("links", mapper.createArrayNode());
pathNode.set("labels", mapper.createArrayNode());
pathNodes.put(tc, pathNode);
paths.add(pathNode);
}
((ArrayNode) pathNode.path("links")).add(compactLinkString(biLink.one));
((ArrayNode) pathNode.path("labels")).add(hasTraffic ? formatBytes(biLink.bytes) : "");
}
return envelope("showTraffic", sid, payload);
}
// Classifies the link traffic according to the specified classes.
private Map<LinkKey, BiLink> classifyLinkTraffic(TrafficClass... trafficClasses) {
Map<LinkKey, BiLink> biLinks = new HashMap<>();
for (TrafficClass trafficClass : trafficClasses) {
for (Intent intent : trafficClass.intents) {
boolean isOptical = intent instanceof OpticalConnectivityIntent;
List<Intent> installables = intentService.getInstallableIntents(intent.key());
if (installables != null) {
for (Intent installable : installables) {
String type = isOptical ? trafficClass.type + " optical" : trafficClass.type;
if (installable instanceof PathIntent) {
classifyLinks(type, biLinks, trafficClass.showTraffic,
((PathIntent) installable).path().links());
} else if (installable instanceof LinkCollectionIntent) {
classifyLinks(type, biLinks, trafficClass.showTraffic,
((LinkCollectionIntent) installable).links());
} else if (installable instanceof OpticalPathIntent) {
classifyLinks(type, biLinks, trafficClass.showTraffic,
((OpticalPathIntent) installable).path().links());
}
}
}
}
}
return biLinks;
}
// Adds the link segments (path or tree) associated with the specified
// connectivity intent
private void classifyLinks(String type, Map<LinkKey, BiLink> biLinks,
boolean showTraffic, Iterable<Link> links) {
if (links != null) {
for (Link link : links) {
BiLink biLink = addLink(biLinks, link);
if (isInfrastructureEgress(link)) {
if (showTraffic) {
biLink.addLoad(statService.load(link));
}
biLink.addClass(type);
}
}
}
}
private BiLink addLink(Map<LinkKey, BiLink> biLinks, Link link) {
LinkKey key = canonicalLinkKey(link);
BiLink biLink = biLinks.get(key);
if (biLink != null) {
biLink.setOther(link);
} else {
biLink = new BiLink(key, link);
biLinks.put(key, biLink);
}
return biLink;
}
// Adds the link segments (path or tree) associated with the specified
// connectivity intent
protected void addPathTraffic(ArrayNode paths, String type, String trafficType,
Iterable<Link> links) {
ObjectNode pathNode = mapper.createObjectNode();
ArrayNode linksNode = mapper.createArrayNode();
if (links != null) {
ArrayNode labels = mapper.createArrayNode();
boolean hasTraffic = false;
for (Link link : links) {
if (isInfrastructureEgress(link)) {
linksNode.add(compactLinkString(link));
Load load = statService.load(link);
String label = "";
if (load.rate() > 0) {
hasTraffic = true;
label = formatBytes(load.latest());
}
labels.add(label);
}
}
pathNode.put("class", hasTraffic ? type + " " + trafficType : type);
pathNode.put("traffic", hasTraffic);
pathNode.set("links", linksNode);
pathNode.set("labels", labels);
paths.add(pathNode);
}
}
// Poor-mans formatting to get the labels with byte counts looking nice.
private String formatBytes(long bytes) {
String unit;
double value;
if (bytes > GB) {
value = bytes / GB;
unit = GB_UNIT;
} else if (bytes > MB) {
value = bytes / MB;
unit = MB_UNIT;
} else if (bytes > KB) {
value = bytes / KB;
unit = KB_UNIT;
} else {
value = bytes;
unit = B_UNIT;
}
DecimalFormat format = new DecimalFormat("#,###.##");
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;
}
// 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());
}
// Produces JSON property details.
private ObjectNode json(String id, String type, Prop... props) {
ObjectMapper mapper = new ObjectMapper();
ObjectNode result = mapper.createObjectNode()
.put("id", id).put("type", type);
ObjectNode pnode = mapper.createObjectNode();
ArrayNode porder = mapper.createArrayNode();
for (Prop p : props) {
porder.add(p.key);
pnode.put(p.key, p.value);
}
result.set("propOrder", porder);
result.set("props", pnode);
return result;
}
// Produces canonical link key, i.e. one that will match link and its inverse.
private LinkKey canonicalLinkKey(Link link) {
String sn = link.src().elementId().toString();
String dn = link.dst().elementId().toString();
return sn.compareTo(dn) < 0 ?
linkKey(link.src(), link.dst()) : linkKey(link.dst(), link.src());
}
// Representation of link and its inverse and any traffic data.
private class BiLink {
public final LinkKey key;
public final Link one;
public Link two;
public boolean hasTraffic = false;
public long bytes = 0;
public String classes = "";
BiLink(LinkKey key, Link link) {
this.key = key;
this.one = link;
}
void setOther(Link link) {
this.two = link;
}
void addLoad(Load load) {
if (load != null) {
this.hasTraffic = hasTraffic || load.rate() > 0;
this.bytes += load.latest();
}
}
void addClass(String trafficClass) {
classes = classes + " " + trafficClass;
}
}
// Auxiliary key/value carrier.
private class Prop {
public final String key;
public final String value;
protected Prop(String key, String value) {
this.key = key;
this.value = value;
}
}
// Auxiliary properties separator
private class Separator extends Prop {
protected Separator() {
super("-", "");
}
}
// Auxiliary carrier of data for requesting traffic message.
protected class TrafficClass {
public final boolean showTraffic;
public final String type;
public final Iterable<Intent> intents;
TrafficClass(String type, Iterable<Intent> intents) {
this(type, intents, false);
}
TrafficClass(String type, Iterable<Intent> intents, boolean showTraffic) {
this.type = type;
this.intents = intents;
this.showTraffic = showTraffic;
}
}
}
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui.impl;
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;
import org.eclipse.jetty.websocket.WebSocket;
import org.onlab.osgi.ServiceDirectory;
import org.onlab.util.AbstractAccumulator;
import org.onlab.util.Accumulator;
import org.onosproject.cluster.ClusterEvent;
import org.onosproject.cluster.ClusterEventListener;
import org.onosproject.cluster.ControllerNode;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.event.Event;
import org.onosproject.mastership.MastershipAdminService;
import org.onosproject.mastership.MastershipEvent;
import org.onosproject.mastership.MastershipListener;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.Device;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
import org.onosproject.net.HostLocation;
import org.onosproject.net.Link;
import org.onosproject.net.device.DeviceEvent;
import org.onosproject.net.device.DeviceListener;
import org.onosproject.net.flow.DefaultTrafficSelector;
import org.onosproject.net.flow.DefaultTrafficTreatment;
import org.onosproject.net.flow.FlowRuleEvent;
import org.onosproject.net.flow.FlowRuleListener;
import org.onosproject.net.flow.TrafficSelector;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.host.HostEvent;
import org.onosproject.net.host.HostListener;
import org.onosproject.net.intent.HostToHostIntent;
import org.onosproject.net.intent.Intent;
import org.onosproject.net.intent.IntentEvent;
import org.onosproject.net.intent.IntentListener;
import org.onosproject.net.intent.MultiPointToSinglePointIntent;
import org.onosproject.net.link.LinkEvent;
import org.onosproject.net.link.LinkListener;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
import static org.onosproject.net.DeviceId.deviceId;
import static org.onosproject.net.HostId.hostId;
import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_UPDATED;
import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED;
import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
/**
* Web socket capable of interacting with the GUI topology view.
*/
@Deprecated
public class TopologyViewWebSocket
extends TopologyViewMessages
implements WebSocket.OnTextMessage, WebSocket.OnControl {
private static final long MAX_AGE_MS = 15000;
private static final byte PING = 0x9;
private static final byte PONG = 0xA;
private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
private static final String APP_ID = "org.onosproject.gui";
private static final long TRAFFIC_FREQUENCY = 5000;
private static final long SUMMARY_FREQUENCY = 30000;
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 Timer timer = new Timer("topology-view");
private static final int MAX_EVENTS = 1000;
private static final int MAX_BATCH_MS = 5000;
private static final int MAX_IDLE_MS = 1000;
private final ApplicationId appId;
private Connection connection;
private FrameConnection control;
private final ClusterEventListener clusterListener = new InternalClusterListener();
private final MastershipListener mastershipListener = new InternalMastershipListener();
private final DeviceListener deviceListener = new InternalDeviceListener();
private final LinkListener linkListener = new InternalLinkListener();
private final HostListener hostListener = new InternalHostListener();
private final IntentListener intentListener = new InternalIntentListener();
private final FlowRuleListener flowListener = new InternalFlowListener();
private final Accumulator<Event> eventAccummulator = new InternalEventAccummulator();
private TimerTask trafficTask;
private ObjectNode trafficEvent;
private TimerTask summaryTask;
private ObjectNode summaryEvent;
private long lastActive = System.currentTimeMillis();
private boolean listenersRemoved = false;
private TopologyViewIntentFilter intentFilter;
// Current selection context
private Set<Host> selectedHosts;
private Set<Device> selectedDevices;
private List<Intent> selectedIntents;
private int currentIntentIndex = -1;
/**
* Creates a new web-socket for serving data to GUI topology view.
*
* @param directory service directory
*/
public TopologyViewWebSocket(ServiceDirectory directory) {
super(directory);
intentFilter = new TopologyViewIntentFilter(intentService, deviceService,
hostService, linkService);
appId = directory.get(CoreService.class).registerApplication(APP_ID);
}
/**
* Issues a close on the connection.
*/
synchronized void close() {
removeListeners();
if (connection.isOpen()) {
connection.close();
}
}
/**
* Indicates if this connection is idle.
*
* @return true if idle or closed
*/
synchronized boolean isIdle() {
boolean idle = (System.currentTimeMillis() - lastActive) > MAX_AGE_MS;
if (idle || (connection != null && !connection.isOpen())) {
return true;
} else if (connection != null) {
try {
control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
} catch (IOException e) {
log.warn("Unable to send ping message due to: ", e);
}
}
return false;
}
@Override
public void onOpen(Connection connection) {
log.info("Legacy GUI client connected");
this.connection = connection;
this.control = (FrameConnection) connection;
addListeners();
sendAllInstances(null);
sendAllDevices();
sendAllLinks();
sendAllHosts();
}
@Override
public synchronized void onClose(int closeCode, String message) {
removeListeners();
timer.cancel();
log.info("Legacy GUI client disconnected");
}
@Override
public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
lastActive = System.currentTimeMillis();
return true;
}
@Override
public void onMessage(String data) {
lastActive = System.currentTimeMillis();
try {
processMessage((ObjectNode) mapper.reader().readTree(data));
} catch (Exception e) {
log.warn("Unable to parse GUI request {} due to {}", data, e);
log.debug("Boom!!!", e);
}
}
// Processes the specified event.
private void processMessage(ObjectNode event) {
String type = string(event, "event", "unknown");
if (type.equals("requestDetails")) {
requestDetails(event);
} else if (type.equals("updateMeta")) {
updateMetaUi(event);
} else if (type.equals("addHostIntent")) {
createHostIntent(event);
} else if (type.equals("addMultiSourceIntent")) {
createMultiSourceIntent(event);
} else if (type.equals("requestRelatedIntents")) {
stopTrafficMonitoring();
requestRelatedIntents(event);
} else if (type.equals("requestNextRelatedIntent")) {
stopTrafficMonitoring();
requestAnotherRelatedIntent(event, +1);
} else if (type.equals("requestPrevRelatedIntent")) {
stopTrafficMonitoring();
requestAnotherRelatedIntent(event, -1);
} else if (type.equals("requestSelectedIntentTraffic")) {
requestSelectedIntentTraffic(event);
startTrafficMonitoring(event);
} else if (type.equals("requestAllTraffic")) {
requestAllTraffic(event);
startTrafficMonitoring(event);
} else if (type.equals("requestDeviceLinkFlows")) {
requestDeviceLinkFlows(event);
startTrafficMonitoring(event);
} else if (type.equals("cancelTraffic")) {
cancelTraffic(event);
} else if (type.equals("requestSummary")) {
requestSummary(event);
startSummaryMonitoring(event);
} else if (type.equals("cancelSummary")) {
stopSummaryMonitoring();
} else if (type.equals("equalizeMasters")) {
equalizeMasters(event);
}
}
// Sends the specified data to the client.
protected synchronized void sendMessage(ObjectNode data) {
try {
if (connection.isOpen()) {
connection.sendMessage(data.toString());
}
} catch (IOException e) {
log.warn("Unable to send message {} to GUI due to {}", data, e);
log.debug("Boom!!!", e);
}
}
// Sends all controller nodes to the client as node-added messages.
private void sendAllInstances(String messageType) {
List<ControllerNode> nodes = new ArrayList<>(clusterService.getNodes());
Collections.sort(nodes, NODE_COMPARATOR);
for (ControllerNode node : nodes) {
sendMessage(instanceMessage(new ClusterEvent(INSTANCE_ADDED, node),
messageType));
}
}
// Sends all devices to the client as device-added messages.
private void sendAllDevices() {
// Send optical first, others later for layered rendering
for (Device device : deviceService.getDevices()) {
if (device.type() == Device.Type.ROADM) {
sendMessage(deviceMessage(new DeviceEvent(DEVICE_ADDED, device)));
}
}
for (Device device : deviceService.getDevices()) {
if (device.type() != Device.Type.ROADM) {
sendMessage(deviceMessage(new DeviceEvent(DEVICE_ADDED, device)));
}
}
}
// Sends all links to the client as link-added messages.
private void sendAllLinks() {
// Send optical first, others later for layered rendering
for (Link link : linkService.getLinks()) {
if (link.type() == Link.Type.OPTICAL) {
sendMessage(linkMessage(new LinkEvent(LINK_ADDED, link)));
}
}
for (Link link : linkService.getLinks()) {
if (link.type() != Link.Type.OPTICAL) {
sendMessage(linkMessage(new LinkEvent(LINK_ADDED, link)));
}
}
}
// Sends all hosts to the client as host-added messages.
private void sendAllHosts() {
for (Host host : hostService.getHosts()) {
sendMessage(hostMessage(new HostEvent(HOST_ADDED, host)));
}
}
// Sends back device or host details.
private void requestDetails(ObjectNode event) {
ObjectNode payload = payload(event);
String type = string(payload, "class", "unknown");
long sid = number(event, "sid");
if (type.equals("device")) {
sendMessage(deviceDetails(deviceId(string(payload, "id")), sid));
} else if (type.equals("host")) {
sendMessage(hostDetails(hostId(string(payload, "id")), sid));
}
}
// Creates host-to-host intent.
private void createHostIntent(ObjectNode event) {
ObjectNode payload = payload(event);
long id = number(event, "sid");
// TODO: add protection against device ids and non-existent hosts.
HostId one = hostId(string(payload, "one"));
HostId two = hostId(string(payload, "two"));
HostToHostIntent intent =
HostToHostIntent.builder()
.appId(appId)
.one(one)
.two(two)
.build();
intentService.submit(intent);
startMonitoringIntent(event, intent);
}
// Creates multi-source-to-single-dest intent.
private void createMultiSourceIntent(ObjectNode event) {
ObjectNode payload = payload(event);
long id = number(event, "sid");
// TODO: add protection against device ids and non-existent hosts.
Set<HostId> src = getHostIds((ArrayNode) payload.path("src"));
HostId dst = hostId(string(payload, "dst"));
Host dstHost = hostService.getHost(dst);
Set<ConnectPoint> ingressPoints = getHostLocations(src);
// FIXME: clearly, this is not enough
TrafficSelector selector = DefaultTrafficSelector.builder()
.matchEthDst(dstHost.mac()).build();
TrafficTreatment treatment = DefaultTrafficTreatment.emptyTreatment();
MultiPointToSinglePointIntent intent =
MultiPointToSinglePointIntent.builder()
.appId(appId)
.selector(selector)
.treatment(treatment)
.ingressPoints(ingressPoints)
.egressPoint(dstHost.location())
.build();
intentService.submit(intent);
startMonitoringIntent(event, intent);
}
private synchronized void startMonitoringIntent(ObjectNode event, Intent intent) {
selectedHosts = new HashSet<>();
selectedDevices = new HashSet<>();
selectedIntents = new ArrayList<>();
selectedIntents.add(intent);
currentIntentIndex = -1;
requestAnotherRelatedIntent(event, +1);
requestSelectedIntentTraffic(event);
}
private Set<ConnectPoint> getHostLocations(Set<HostId> hostIds) {
Set<ConnectPoint> points = new HashSet<>();
for (HostId hostId : hostIds) {
points.add(getHostLocation(hostId));
}
return points;
}
private HostLocation getHostLocation(HostId hostId) {
return hostService.getHost(hostId).location();
}
// Produces a list of host ids from the specified JSON array.
private Set<HostId> getHostIds(ArrayNode ids) {
Set<HostId> hostIds = new HashSet<>();
for (JsonNode id : ids) {
hostIds.add(hostId(id.asText()));
}
return hostIds;
}
private synchronized long startTrafficMonitoring(ObjectNode event) {
stopTrafficMonitoring();
trafficEvent = event;
trafficTask = new TrafficMonitor();
timer.schedule(trafficTask, TRAFFIC_FREQUENCY, TRAFFIC_FREQUENCY);
return number(event, "sid");
}
private synchronized void stopTrafficMonitoring() {
if (trafficTask != null) {
trafficTask.cancel();
trafficTask = null;
trafficEvent = null;
}
}
// Subscribes for host traffic messages.
private synchronized void requestAllTraffic(ObjectNode event) {
long sid = startTrafficMonitoring(event);
sendMessage(trafficSummaryMessage(sid));
}
private void requestDeviceLinkFlows(ObjectNode event) {
ObjectNode payload = payload(event);
long sid = startTrafficMonitoring(event);
// Get the set of selected hosts and their intents.
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 = string(payload, "hover");
if (!isNullOrEmpty(hover)) {
addHover(hosts, devices, hover);
}
sendMessage(flowSummaryMessage(sid, devices));
}
// Requests related intents message.
private synchronized void requestRelatedIntents(ObjectNode event) {
ObjectNode payload = payload(event);
if (!payload.has("ids")) {
return;
}
long sid = number(event, "sid");
// Cancel any other traffic monitoring mode.
stopTrafficMonitoring();
// Get the set of selected hosts and their intents.
ArrayNode ids = (ArrayNode) payload.path("ids");
selectedHosts = getHosts(ids);
selectedDevices = getDevices(ids);
selectedIntents = intentFilter.findPathIntents(selectedHosts, selectedDevices,
intentService.getIntents());
currentIntentIndex = -1;
if (haveSelectedIntents()) {
// Send a message to highlight all links of all monitored intents.
sendMessage(trafficMessage(sid, new TrafficClass("primary", selectedIntents)));
}
// FIXME: Re-introduce one the client click vs hover gesture stuff is sorted out.
// String hover = string(payload, "hover");
// if (!isNullOrEmpty(hover)) {
// // If there is a hover node, include it in the selection and find intents.
// processHoverExtendedSelection(sid, hover);
// }
}
private boolean haveSelectedIntents() {
return selectedIntents != null && !selectedIntents.isEmpty();
}
// Processes the selection extended with hovered item to segregate items
// into primary (those including the hover) vs secondary highlights.
private void processHoverExtendedSelection(long sid, String hover) {
Set<Host> hoverSelHosts = new HashSet<>(selectedHosts);
Set<Device> hoverSelDevices = new HashSet<>(selectedDevices);
addHover(hoverSelHosts, hoverSelDevices, hover);
List<Intent> primary = selectedIntents == null ? new ArrayList<>() :
intentFilter.findPathIntents(hoverSelHosts, hoverSelDevices,
selectedIntents);
Set<Intent> secondary = new HashSet<>(selectedIntents);
secondary.removeAll(primary);
// Send a message to highlight all links of all monitored intents.
sendMessage(trafficMessage(sid, new TrafficClass("primary", primary),
new TrafficClass("secondary", secondary)));
}
// Requests next or previous related intent.
private void requestAnotherRelatedIntent(ObjectNode event, int offset) {
if (haveSelectedIntents()) {
currentIntentIndex = currentIntentIndex + offset;
if (currentIntentIndex < 0) {
currentIntentIndex = selectedIntents.size() - 1;
} else if (currentIntentIndex >= selectedIntents.size()) {
currentIntentIndex = 0;
}
sendSelectedIntent(event);
}
}
// Sends traffic information on the related intents with the currently
// selected intent highlighted.
private void sendSelectedIntent(ObjectNode event) {
Intent selectedIntent = selectedIntents.get(currentIntentIndex);
log.info("Requested next intent {}", selectedIntent.id());
Set<Intent> primary = new HashSet<>();
primary.add(selectedIntent);
Set<Intent> secondary = new HashSet<>(selectedIntents);
secondary.remove(selectedIntent);
// Send a message to highlight all links of the selected intent.
sendMessage(trafficMessage(number(event, "sid"),
new TrafficClass("primary", primary),
new TrafficClass("secondary", secondary)));
}
// Requests monitoring of traffic for the selected intent.
private void requestSelectedIntentTraffic(ObjectNode event) {
if (haveSelectedIntents()) {
if (currentIntentIndex < 0) {
currentIntentIndex = 0;
}
Intent selectedIntent = selectedIntents.get(currentIntentIndex);
log.info("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(number(event, "sid"),
new TrafficClass("primary", primary, true)));
}
}
// Cancels sending traffic messages.
private void cancelTraffic(ObjectNode event) {
selectedIntents = null;
sendMessage(trafficMessage(number(event, "sid")));
stopTrafficMonitoring();
}
private synchronized long startSummaryMonitoring(ObjectNode event) {
stopSummaryMonitoring();
summaryEvent = event;
summaryTask = new SummaryMonitor();
timer.schedule(summaryTask, SUMMARY_FREQUENCY, SUMMARY_FREQUENCY);
return number(event, "sid");
}
private synchronized void stopSummaryMonitoring() {
if (summaryEvent != null) {
summaryTask.cancel();
summaryTask = null;
summaryEvent = null;
}
}
// Subscribes for summary messages.
private synchronized void requestSummary(ObjectNode event) {
sendMessage(summmaryMessage(number(event, "sid")));
}
// Forces mastership role rebalancing.
private void equalizeMasters(ObjectNode event) {
directory.get(MastershipAdminService.class).balanceRoles();
}
// Adds all internal listeners.
private void addListeners() {
clusterService.addListener(clusterListener);
mastershipService.addListener(mastershipListener);
deviceService.addListener(deviceListener);
linkService.addListener(linkListener);
hostService.addListener(hostListener);
intentService.addListener(intentListener);
flowService.addListener(flowListener);
}
// Removes all internal listeners.
private synchronized void removeListeners() {
if (!listenersRemoved) {
listenersRemoved = true;
clusterService.removeListener(clusterListener);
mastershipService.removeListener(mastershipListener);
deviceService.removeListener(deviceListener);
linkService.removeListener(linkListener);
hostService.removeListener(hostListener);
intentService.removeListener(intentListener);
flowService.removeListener(flowListener);
}
}
// Cluster event listener.
private class InternalClusterListener implements ClusterEventListener {
@Override
public void event(ClusterEvent event) {
sendMessage(instanceMessage(event, null));
}
}
// Mastership change listener
private class InternalMastershipListener implements MastershipListener {
@Override
public void event(MastershipEvent event) {
sendAllInstances("updateInstance");
Device device = deviceService.getDevice(event.subject());
sendMessage(deviceMessage(new DeviceEvent(DEVICE_UPDATED, device)));
}
}
// Device event listener.
private class InternalDeviceListener implements DeviceListener {
@Override
public void event(DeviceEvent event) {
sendMessage(deviceMessage(event));
eventAccummulator.add(event);
}
}
// Link event listener.
private class InternalLinkListener implements LinkListener {
@Override
public void event(LinkEvent event) {
sendMessage(linkMessage(event));
eventAccummulator.add(event);
}
}
// Host event listener.
private class InternalHostListener implements HostListener {
@Override
public void event(HostEvent event) {
sendMessage(hostMessage(event));
eventAccummulator.add(event);
}
}
// Intent event listener.
private class InternalIntentListener implements IntentListener {
@Override
public void event(IntentEvent event) {
if (trafficEvent != null) {
requestSelectedIntentTraffic(trafficEvent);
}
eventAccummulator.add(event);
}
}
// Intent event listener.
private class InternalFlowListener implements FlowRuleListener {
@Override
public void event(FlowRuleEvent event) {
eventAccummulator.add(event);
}
}
// Periodic update of the traffic information
private class TrafficMonitor extends TimerTask {
@Override
public void run() {
try {
if (trafficEvent != null) {
String type = string(trafficEvent, "event", "unknown");
if (type.equals("requestAllTraffic")) {
requestAllTraffic(trafficEvent);
} else if (type.equals("requestDeviceLinkFlows")) {
requestDeviceLinkFlows(trafficEvent);
} else if (type.equals("requestSelectedIntentTraffic")) {
requestSelectedIntentTraffic(trafficEvent);
}
}
} catch (Exception e) {
log.warn("Unable to handle traffic request due to {}", e.getMessage());
log.debug("Boom!", e);
}
}
}
// Periodic update of the summary information
private class SummaryMonitor extends TimerTask {
@Override
public void run() {
try {
if (summaryEvent != null) {
requestSummary(summaryEvent);
}
} catch (Exception e) {
log.warn("Unable to handle summary request due to {}", e.getMessage());
log.debug("Boom!", e);
}
}
}
// Accumulates events to drive methodic update of the summary pane.
private class InternalEventAccummulator extends AbstractAccumulator<Event> {
protected InternalEventAccummulator() {
super(new Timer("topo-summary"), MAX_EVENTS, MAX_BATCH_MS, MAX_IDLE_MS);
}
@Override
public void processItems(List<Event> items) {
try {
if (summaryEvent != null) {
sendMessage(summmaryMessage(0));
}
} catch (Exception e) {
log.warn("Unable to handle summary request due to {}", e.getMessage());
log.debug("Boom!", e);
}
}
}
}
......@@ -161,15 +161,4 @@
<url-pattern>/websock/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>Legacy Web Socket Service</servlet-name>
<servlet-class>org.onosproject.ui.impl.GuiWebSocketServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Legacy Web Socket Service</servlet-name>
<url-pattern>/ws/*</url-pattern>
</servlet-mapping>
</web-app>
......
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Base CSS file
*/
html {
font-family: sans-serif;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body {
margin: 0;
}
This diff could not be displayed because it is too large.
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
D3 Utilities CSS file
*/
#d3u .light.norm.c1 {
color: #1f77b4;
}
#d3u .light.norm.c2 {
color: #2ca02c;
}
#d3u .light.norm.c3 {
color: #d62728;
}
#d3u .light.norm.c4 {
color: #9467bd;
}
#d3u .light.norm.c5 {
color: #e377c2;
}
#d3u .light.norm.c6 {
color: #bcbd22;
}
#d3u .light.norm.c7 {
color: #17becf;
}
#d3u .light.mute.c1 {
color: #aec7e8;
}
#d3u .light.mute.c2 {
color: #98df8a;
}
#d3u .light.mute.c3 {
color: #ff9896;
}
#d3u .light.mute.c4 {
color: #c5b0d5;
}
#d3u .light.mute.c5 {
color: #f7b6d2;
}
#d3u .light.mute.c6 {
color: #dbdb8d;
}
#d3u .light.mute.c7 {
color: #9edae5;
}
#d3u .dark.norm.c1 {
color: #1f77b4;
}
#d3u .dark.norm.c2 {
color: #2ca02c;
}
#d3u .dark.norm.c3 {
color: #d62728;
}
#d3u .dark.norm.c4 {
color: #9467bd;
}
#d3u .dark.norm.c5 {
color: #e377c2;
}
#d3u .dark.norm.c6 {
color: #bcbd22;
}
#d3u .dark.norm.c7 {
color: #17becf;
}
#d3u .dark.mute.c1 {
color: #aec7e8;
}
#d3u .dark.mute.c2 {
color: #639a56;
}
#d3u .dark.mute.c3 {
color: #ff9896;
}
#d3u .dark.mute.c4 {
color: #c5b0d5;
}
#d3u .dark.mute.c5 {
color: #f7b6d2;
}
#d3u .dark.mute.c6 {
color: #dbdb8d;
}
#d3u .dark.mute.c7 {
color: #9edae5;
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Utility functions for D3 visualizations.
*/
(function (onos) {
'use strict';
function isF(f) {
return $.isFunction(f) ? f : null;
}
// TODO: sensible defaults
function createDragBehavior(force, selectCb, atDragEnd,
dragEnabled, clickEnabled) {
var draggedThreshold = d3.scale.linear()
.domain([0, 0.1])
.range([5, 20])
.clamp(true),
drag,
fSel = isF(selectCb),
fEnd = isF(atDragEnd),
fDEn = isF(dragEnabled),
fCEn = isF(clickEnabled),
bad = [];
function naf(what) {
return 'd3util.createDragBehavior(): '+ what + ' is not a function';
}
if (!fSel) {
bad.push(naf('selectCb'));
}
if (!fEnd) {
bad.push(naf('atDragEnd'));
}
if (!fDEn) {
bad.push(naf('dragEnabled'));
}
if (!fCEn) {
bad.push(naf('clickEnabled'));
}
if (bad.length) {
alert(bad.join('\n'));
return null;
}
function dragged(d) {
var threshold = draggedThreshold(force.alpha()),
dx = d.oldX - d.px,
dy = d.oldY - d.py;
if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
d.dragged = true;
}
return d.dragged;
}
drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on('dragstart', function(d) {
if (clickEnabled() || dragEnabled()) {
d3.event.sourceEvent.stopPropagation();
d.oldX = d.x;
d.oldY = d.y;
d.dragged = false;
d.fixed |= 2;
d.dragStarted = true;
}
})
.on('drag', function(d) {
if (dragEnabled()) {
d.px = d3.event.x;
d.py = d3.event.y;
if (dragged(d)) {
if (!force.alpha()) {
force.alpha(.025);
}
}
}
})
.on('dragend', function(d) {
if (d.dragStarted) {
d.dragStarted = false;
if (!dragged(d)) {
// consider this the same as a 'click'
// (selection of a node)
if (clickEnabled()) {
selectCb(d, this); // TODO: set 'this' context instead of param
}
}
d.fixed &= ~6;
// hook at the end of a drag gesture
if (dragEnabled()) {
atDragEnd(d, this); // TODO: set 'this' context instead of param
}
}
});
return drag;
}
function loadGlow(defs) {
// TODO: parameterize color
var glow = defs.append('filter')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%')
.attr('id', 'blue-glow');
glow.append('feColorMatrix')
.attr('type', 'matrix')
.attr('values',
'0 0 0 0 0 ' +
'0 0 0 0 0 ' +
'0 0 0 0 .7 ' +
'0 0 0 1 0 ');
glow.append('feGaussianBlur')
.attr('stdDeviation', 3)
.attr('result', 'coloredBlur');
glow.append('feMerge').selectAll('feMergeNode')
.data(['coloredBlur', 'SourceGraphic'])
.enter().append('feMergeNode')
.attr('in', String);
}
// --- Ordinal scales for 7 values.
// TODO: tune colors for light and dark themes
// Note: These colors look good on the white background. Still, need to tune for dark.
// blue brown brick red sea green purple dark teal lime
var lightNorm = ['#3E5780', '#78533B', '#CB4D28', '#018D61', '#8A2979', '#006D73', '#56AF00'],
lightMute = ['#A8B8CC', '#CCB3A8', '#FFC2BD', '#96D6BF', '#D19FCE', '#8FCCCA', '#CAEAA4'],
darkNorm = ['#3E5780', '#78533B', '#CB4D28', '#018D61', '#8A2979', '#006D73', '#56AF00'],
darkMute = ['#A8B8CC', '#CCB3A8', '#FFC2BD', '#96D6BF', '#D19FCE', '#8FCCCA', '#CAEAA4'];
function cat7() {
var colors = {
light: {
norm: d3.scale.ordinal().range(lightNorm),
mute: d3.scale.ordinal().range(lightMute)
},
dark: {
norm: d3.scale.ordinal().range(darkNorm),
mute: d3.scale.ordinal().range(darkMute)
}
},
tcid = 'd3utilTestCard';
function get(id, muted, theme) {
// NOTE: since we are lazily assigning domain ids, we need to
// get the color from all 4 scales, to keep the domains
// in sync.
var ln = colors.light.norm(id),
lm = colors.light.mute(id),
dn = colors.dark.norm(id),
dm = colors.dark.mute(id);
if (theme === 'dark') {
return muted ? dm : dn;
} else {
return muted ? lm : ln;
}
}
function testCard(svg) {
var g = svg.select('g#' + tcid),
dom = d3.range(7),
k, muted, theme, what;
if (!g.empty()) {
g.remove();
} else {
g = svg.append('g')
.attr('id', tcid)
.attr('transform', 'scale(4)translate(20,20)');
for (k=0; k<4; k++) {
muted = k%2;
what = muted ? ' muted' : ' normal';
theme = k < 2 ? 'light' : 'dark';
dom.forEach(function (id, i) {
var x = i * 20,
y = k * 20,
f = get(id, muted, theme);
g.append('circle').attr({
cx: x,
cy: y,
r: 5,
fill: f
});
});
g.append('rect').attr({
x: 140,
y: k * 20 - 5,
width: 32,
height: 10,
rx: 2,
fill: '#888'
});
g.append('text').text(theme + what)
.attr({
x: 142,
y: k * 20 + 2,
fill: 'white'
})
.style('font-size', '4pt');
}
}
}
return {
testCard: testCard,
get: get
};
}
// === register the functions as a library
onos.ui.addLib('d3util', {
createDragBehavior: createDragBehavior,
loadGlow: loadGlow,
cat7: cat7
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Geometry library - based on work by Mike Bostock.
*/
(function() {
if (typeof geo == 'undefined') {
geo = {};
}
var tolerance = 1e-10;
function eq(a, b) {
return (Math.abs(a - b) < tolerance);
}
function gt(a, b) {
return (a - b > -tolerance);
}
function lt(a, b) {
return gt(b, a);
}
geo.eq = eq;
geo.gt = gt;
geo.lt = lt;
geo.LineSegment = function(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
// Ax + By = C
this.a = y2 - y1;
this.b = x1 - x2;
this.c = x1 * this.a + y1 * this.b;
if (eq(this.a, 0) && eq(this.b, 0)) {
throw new Error(
'Cannot construct a LineSegment with two equal endpoints.');
}
};
geo.LineSegment.prototype.intersect = function(that) {
var d = (this.x1 - this.x2) * (that.y1 - that.y2) -
(this.y1 - this.y2) * (that.x1 - that.x2);
if (eq(d, 0)) {
// The two lines are parallel or very close.
return {
x : NaN,
y : NaN
};
}
var t1 = this.x1 * this.y2 - this.y1 * this.x2,
t2 = that.x1 * that.y2 - that.y1 * that.x2,
x = (t1 * (that.x1 - that.x2) - t2 * (this.x1 - this.x2)) / d,
y = (t1 * (that.y1 - that.y2) - t2 * (this.y1 - this.y2)) / d,
in1 = (gt(x, Math.min(this.x1, this.x2)) && lt(x, Math.max(this.x1, this.x2)) &&
gt(y, Math.min(this.y1, this.y2)) && lt(y, Math.max(this.y1, this.y2))),
in2 = (gt(x, Math.min(that.x1, that.x2)) && lt(x, Math.max(that.x1, that.x2)) &&
gt(y, Math.min(that.y1, that.y2)) && lt(y, Math.max(that.y1, that.y2)));
return {
x : x,
y : y,
in1 : in1,
in2 : in2
};
};
geo.LineSegment.prototype.x = function(y) {
// x = (C - By) / a;
if (this.a) {
return (this.c - this.b * y) / this.a;
} else {
// a == 0 -> horizontal line
return NaN;
}
};
geo.LineSegment.prototype.y = function(x) {
// y = (C - Ax) / b;
if (this.b) {
return (this.c - this.a * x) / this.b;
} else {
// b == 0 -> vertical line
return NaN;
}
};
geo.LineSegment.prototype.length = function() {
return Math.sqrt(
(this.y2 - this.y1) * (this.y2 - this.y1) +
(this.x2 - this.x1) * (this.x2 - this.x1));
};
geo.LineSegment.prototype.offset = function(x, y) {
return new geo.LineSegment(
this.x1 + x, this.y1 + y,
this.x2 + x, this.y2 + y);
};
})();
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS -- SVG Glyphs Library.
*/
(function (onos) {
'use strict';
var birdViewBox = '352 224 113 112',
birdData = {
bird: "M427.7,300.4 c-6.9,0.6-13.1,5-19.2,7.1c-18.1,6.2-33.9," +
"9.1-56.5,4.7c24.6,17.2,36.6,13,63.7,0.1c-0.5,0.6-0.7,1.3-1.3," +
"1.9c1.4-0.4,2.4-1.7,3.4-2.2c-0.4,0.7-0.9,1.5-1.4,1.9c2.2-0.6," +
"3.7-2.3,5.9-3.9c-2.4,2.1-4.2,5-6,8c-1.5,2.5-3.1,4.8-5.1,6.9c-1," +
"1-1.9,1.9-2.9,2.9c-1.4,1.3-2.9,2.5-5.1,2.9c1.7,0.1,3.6-0.3,6.5" +
"-1.9c-1.6,2.4-7.1,6.2-9.9,7.2c10.5-2.6,19.2-15.9,25.7-18c18.3" +
"-5.9,13.8-3.4,27-14.2c1.6-1.3,3-1,5.1-0.8c1.1,0.1,2.1,0.3,3.2," +
"0.5c0.8,0.2,1.4,0.4,2.2,0.8l1.8,0.9c-1.9-4.5-2.3-4.1-5.9-6c-2.3" +
"-1.3-3.3-3.8-6.2-4.9c-7.1-2.6-11.9,11.7-11.7-5c0.1-8,4.2-14.4," +
"6.4-22c1.1-3.8,2.3-7.6,2.4-11.5c0.1-2.3,0-4.7-0.4-7c-2-11.2-8.4" +
"-21.5-19.7-24.8c-1-0.3-1.1-0.3-0.9,0c9.6,17.1,7.2,38.3,3.1,54.2" +
"C429.9,285.5,426.7,293.2,427.7,300.4z"
},
glyphViewBox = '0 0 110 110',
glyphData = {
unknown: "M35,40a5,5,0,0,1,5-5h30a5,5,0,0,1,5,5v30a5,5,0,0,1-5,5" +
"h-30a5,5,0,0,1-5-5z",
node: "M15,100a5,5,0,0,1-5-5v-65a5,5,0,0,1,5-5h80a5,5,0,0,1,5,5" +
"v65a5,5,0,0,1-5,5zM14,22.5l11-11a10,3,0,0,1,10-2h40a10,3,0,0,1," +
"10,2l11,11zM16,35a5,5,0,0,1,10,0a5,5,0,0,1-10,0z",
switch: "M10,20a10,10,0,0,1,10-10h70a10,10,0,0,1,10,10v70a10,10," +
"0,0,1-10,10h-70a10,10,0,0,1-10-10zM60,26l12,0,0-8,18,13-18,13,0" +
"-8-12,0zM60,60l12,0,0-8,18,13-18,13,0-8-12,0zM50,40l-12,0,0-8" +
"-18,13,18,13,0-8,12,0zM50,74l-12,0,0-8-18,13,18,13,0-8,12,0z",
roadm: "M10,35l25-25h40l25,25v40l-25,25h-40l-25-25zM58,26l12,0,0" +
"-8,18,13-18,13,0-8-12,0zM58,60l12,0,0-8,18,13-18,13,0-8-12,0z" +
"M52,40l-12,0,0-8-18,13,18,13,0-8,12,0zM52,74l-12,0,0-8-18,13," +
"18,13,0-8,12,0z",
endstation: "M10,15a5,5,0,0,1,5-5h65a5,5,0,0,1,5,5v80a5,5,0,0,1" +
"-5,5h-65a5,5,0,0,1-5-5zM87.5,14l11,11a3,10,0,0,1,2,10v40a3,10," +
"0,0,1,-2,10l-11,11zM17,19a2,2,0,0,1,2-2h56a2,2,0,0,1,2,2v26a2," +
"2,0,0,1-2,2h-56a2,2,0,0,1-2-2zM20,20h54v10h-54zM20,33h54v10h" +
"-54zM42,70a5,5,0,0,1,10,0a5,5,0,0,1-10,0z",
router: "M10,55A45,45,0,0,1,100,55A45,45,0,0,1,10,55M20,50l12,0," +
"0-8,18,13-18,13,0-8-12,0zM90,50l-12,0,0-8-18,13,18,13,0-8,12,0z" +
"M50,47l0-12-8,0,13-18,13,18-8,0,0,12zM50,63l0,12-8,0,13,18,13" +
"-18-8,0,0-12z",
bgpSpeaker: "M10,40a45,35,0,0,1,90,0Q100,77,55,100Q10,77,10,40z" +
"M50,29l12,0,0-8,18,13-18,13,0-8-12,0zM60,57l-12,0,0-8-18,13," +
"18,13,0-8,12,0z",
chain: "M60.4,77.6c-4.9,5.2-9.6,11.3-15.3,16.3c-8.6,7.5-20.4,6.8" +
"-28-0.8c-7.7-7.7-8.4-19.6-0.8-28.4c6.5-7.4,13.5-14.4,20.9-20.9" +
"c7.5-6.7,19.2-6.7,26.5-0.8c3.5,2.8,4.4,6.1,2.2,8.7c-2.7,3.1" +
"-5.5,2.5-8.5,0.3c-4.7-3.4-9.7-3.2-14,0.9C37.1,58.7,31,64.8," +
"25.2,71c-4.2,4.4-4.2,10.6-0.6,14.3c3.7,3.7,9.7,3.7,14.3-0.4" +
"c2.9-2.5,5.3-5.5,8.3-8c1-0.9,3-1.1,4.4-0.9C54.8,76.3,57.9,77.1," +
"60.4,77.6zM49.2,32.2c5-5.2,9.7-10.9,15.2-15.7c12.8-11,31.2" +
"-4.9,34.3,11.2C100,34.2,98.3,40.2,94,45c-6.7,7.4-13.7,14.6" +
"-21.2,21.2C65.1,73,53.2,72.7,46,66.5c-3.2-2.8-3.9-5.8-1.6-8.4" +
"c2.6-2.9,5.3-2.4,8.2-0.3c5.2,3.7,10,3.3,14.7-1.1c5.8-5.6,11.6" +
"-11.3,17.2-17.2c4.6-4.8,4.9-11.1,0.9-15c-3.9-3.9-10.1-3.4-15," +
"1.2c-3.1,2.9-5.7,7.4-9.3,8.5C57.6,35.3,53,33,49.2,32.2z",
crown: "M99.5,21.6c0,3-2.3,5.4-5.1,5.4c-0.3,0-0.7,0-1-0.1c-1.8," +
"4-4.8,10-7.2,17.3c-3.4,10.6-0.9,26.2,2.7,27.3C90.4,72,91.3," +
"75,88,75H22.7c-3.3,0-2.4-3-0.9-3.5c3.6-1.1,6.1-16.7,2.7-27.3" +
"c-2.4-7.4-5.4-13.5-7.2-17.5c-0.5,0.2-1,0.3-1.6,0.3c-2.8,0" +
"-5.1-2.4-5.1-5.4c0-3,2.3-5.4,5.1-5.4c2.8,0,5.1,2.4,5.1,5.4c0," +
"1-0.2,1.9-0.7,2.7c0.7,0.8,1.4,1.6,2.4,2.6c8.8,8.9,11.9,12.7," +
"18.1,11.7c6.5-1,11-8.2,13.3-14.1c-2-0.8-3.3-2.7-3.3-5.1c0-3," +
"2.3-5.4,5.1-5.4c2.8,0,5.1,2.4,5.1,5.4c0,2.5-1.6,4.5-3.7,5.2" +
"c2.3,5.9,6.8,13,13.2,14c6.2,1,9.3-2.7,18.1-11.7c0.7-0.7,1.4" +
"-1.5,2-2.1c-0.6-0.9-1-2-1-3.1c0-3,2.3-5.4,5.1-5.4C97.2,16.2," +
"99.5,18.6,99.5,21.6zM92,87.9c0,2.2-1.7,4.1-3.8,4.1H22.4c" +
"-2.1,0-4.4-1.9-4.4-4.1v-3.3c0-2.2,2.3-4.5,4.4-4.5h65.8c2.1," +
"0,3.8,2.3,3.8,4.5V87.9z"
},
badgeViewBox = '0 0 10 10',
badgeData = {
uiAttached: "M2,2.5a.5,.5,0,0,1,.5-.5h5a.5,.5,0,0,1,.5,.5v3" +
"a.5,.5,0,0,1-.5,.5h-5a.5,.5,0,0,1-.5-.5zM2.5,2.8a.3,.3,0,0,1," +
".3-.3h4.4a.3,.3,0,0,1,.3,.3v2.4a.3,.3,0,0,1-.3,.3h-4.4" +
"a.3,.3,0,0,1-.3-.3zM2,6.55h6l1,1.45h-8z"
};
function defStuff(defs, viewbox, data) {
d3.map(data).keys().forEach(function (key) {
defs.append('symbol')
.attr({ id: key, viewBox: viewbox })
.append('path').attr('d', data[key]);
});
}
function loadDefs(defs) {
defStuff(defs, birdViewBox, birdData);
defStuff(defs, glyphViewBox, glyphData);
defStuff(defs, badgeViewBox, badgeData);
}
onos.ui.addLib('glyphs', {
loadDefs: loadDefs
});
}(ONOS));
<!DOCTYPE html>
<!--
~ Copyright 2014 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!--
ONOS UI - single page web app
Version 1.1
-->
<html>
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="../img/onos-logo.png">
<title>ONOS</title>
<!-- Third party library code included here -->
<!--TODO: use the minified version of d3, once debugging is complete -->
<script src="../tp/d3.js"></script>
<script src="../tp/topojson.v1.min.js"></script>
<script src="../tp/jquery-2.1.1.min.js"></script>
<!-- ONOS UI Framework included here -->
<script src="onos.js"></script>
<!-- Framework and library stylesheets included here -->
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="onos.css">
<link rel="stylesheet" href="onosMast.css">
<link rel="stylesheet" href="onosFloatPanel.css">
<link rel="stylesheet" href="onosFlash.css">
<link rel="stylesheet" href="onosQuickHelp.css">
<!-- This is where contributed stylesheets get INJECTED -->
<!-- TODO: replace with template marker and inject refs server-side -->
<link rel="stylesheet" href="topo.css">
</head>
<body>
<div id="frame">
<div id="mast">
<!-- NOTE: masthead injected here by mast.js -->
</div>
<div id="view">
<!-- NOTE: views injected here by onos.js -->
</div>
<div id="floatPanels">
<!-- NOTE: floating panels injected here, as needed -->
<!-- see onos.ui.addFloatingPanel -->
</div>
<div id="alerts">
<!-- NOTE: alert content injected here, as needed -->
</div>
<div id="feedback">
<!-- NOTE: feedback flashes to user -->
</div>
<div id="quickhelp">
<!-- NOTE: key bindings and mouse gesture help -->
</div>
</div>
<!-- Initialize the UI...-->
<script type="text/javascript">
var ONOS = $.onos({
comment: 'configuration options',
theme: 'dark',
startVid: 'topo'
});
</script>
<!-- Library modules included here -->
<script src="d3Utils.js"></script>
<script src="geometry.js"></script>
<script src="glyphs.js"></script>
<!-- Framework modules included here -->
<script src="onosMast.js"></script>
<script src="onosFlash.js"></script>
<script src="onosQuickHelp.js"></script>
<!-- View Module Templates; REMOVE THESE LINES, FOR PRODUCTION -->
<script src="module-svg-template.js"></script>
<script src="module-div-template.js"></script>
<!-- Sample Views; REMOVE THESE LINES, FOR PRODUCTION -->
<script src="sample.js"></script>
<script src="sampleRadio.js"></script>
<script src="sampleKeys.js"></script>
<script src="sampleHash.js"></script>
<!-- Contributed (application) views injected here -->
<!-- TODO: replace with template marker and inject refs server-side -->
<script src="topo.js"></script>
<!-- finally, build the UI-->
<script type="text/javascript">
$(ONOS.buildUi);
</script>
</body>
</html>
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Module template file for DIV based view.
*/
(function (onos) {
'use strict';
var list,
data = [ 'foo', 'bar', 'baz' ];
// invoked only the first time the view is loaded
// - used to initialize the view contents
function init(view, ctx, flags) {
// NOTE: view.$div is a D3 selection of the view's div
list = view.$div.append('ul');
// ... further code to initialize the SVG view ...
}
// invoked just prior to loading the view
// - used to clear the view of stale data
function reset(view, ctx, flags) {
}
// invoked when the view is loaded
// - used to load data into the view,
// when the view is shown
function load(view, ctx, flags) {
list.selectAll('li')
.data(data)
.enter()
.append('li')
.text(function (d) { return d; })
}
// invoked when the view is unloaded
// - used to clean up data that should be removed,
// when the view is hidden
function unload(view, ctx, flags) {
}
// invoked when the view is resized
// - used to reconfigure elements to the new view size
function resize(view, ctx, flags) {
var w = view.width(),
h = view.height();
}
// invoked when the framework needs to alert the view of an error
// - (EXPERIMENTAL -- not currently used)
function error(view, ctx, flags) {
}
// ================================================================
// == register the view here, with links to lifecycle callbacks
// A typical setup that initializes the view once, then reacts to
// load and resize events would look like this:
onos.ui.addView('myDivViewId', {
init: init,
load: load,
resize: resize
});
// A minimum setup that builds the view every time it is loaded
// would look like this:
//
// onos.ui.addView('myViewId', {
// reset: true, // clear view contents on reset
// load: load
// });
// The complete gamut of callbacks would look like this:
//
// onos.ui.addView('myViewId', {
// init: init,
// reset: reset,
// load: load,
// unload: unload,
// resize: resize,
// error: error
// });
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Module template file for SVG based view.
*/
(function (onos) {
'use strict';
var svg,
data = [ 60 ];
// invoked only the first time the view is loaded
// - used to initialize the view contents
function init(view, ctx, flags) {
svg = view.$div.append('svg');
resize(view);
// ... further code to initialize the SVG view ...
}
// invoked just prior to loading the view
// - used to clear the view of stale data
function reset(view, ctx, flags) {
// e.g. clear svg of all objects...
// svg.html('');
}
// invoked when the view is loaded
// - used to load data into the view,
// when the view is shown
function load(view, ctx, flags) {
var w = view.width(),
h = view.height();
// as an example...
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr({
cx: w / 2,
cy: h / 2,
r: function (d) { return d; }
})
.style({
fill: 'goldenrod',
stroke: 'black',
'stroke-width': 3.5,
});
}
// invoked when the view is unloaded
// - used to clean up data that should be removed,
// when the view is hidden
function unload(view, ctx, flags) {
}
// invoked when the view is resized
// - used to reconfigure elements to the new size of the view
function resize(view, ctx, flags) {
var w = view.width(),
h = view.height();
// resize svg layer to match new size of view
svg.attr({
width: w,
height: h
});
// as an example...
svg.selectAll('circle')
.attr({
cx: w / 2,
cy: h / 2
});
// ... further code to handle resize of view ...
}
// invoked when the framework needs to alert the view of an error
// - (EXPERIMENTAL -- not currently used)
function error(view, ctx, flags) {
}
// ================================================================
// == register the view here, with links to lifecycle callbacks
// A typical setup that initializes the view once, then reacts to
// load and resize events would look like this:
onos.ui.addView('mySvgViewId', {
init: init,
load: load,
resize: resize
});
// A minimum setup that builds the view every time it is loaded
// would look like this:
//
// onos.ui.addView('myViewId', {
// reset: true, // clear view contents on reset
// load: load
// });
// The complete gamut of callbacks would look like this:
//
// onos.ui.addView('myViewId', {
// init: init,
// reset: reset,
// load: load,
// unload: unload,
// resize: resize,
// error: error
// });
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Base Framework -- CSS file
*/
html, body {
height: 100%;
}
/* This is to ensure that the body does not expand to account for the
flyout details pane, that is positioned "off screen".
*/
body {
overflow: hidden;
}
#frame {
width: 100%;
height: 100%;
background-color: #fff;
}
div.onosView {
display: none;
}
div.onosView.currentView {
display: block;
}
#alerts {
display: none;
position: absolute;
z-index: 1200;
opacity: 0.65;
background-color: #006;
color: white;
top: 80px;
left: 40px;
padding: 3px 6px;
-moz-border-radius: 6px;
border-radius: 6px;
box-shadow: 0px 2px 12px #777;
}
#alerts pre {
margin: 0.2em 6px;
}
#alerts span.close {
color: #6af;
float: right;
right: 2px;
cursor: pointer;
}
#alerts span.close:hover {
color: #fff;
}
#alerts p.footnote {
text-align: right;
font-size: 8pt;
margin: 8px 0 0 0;
color: #66d;
}
#floatPanels {
z-index: 1100;
}
#flyout {
position: absolute;
display: block;
top: 10%;
width: 280px;
right: -300px;
opacity: 0;
background-color: rgba(255,255,255,0.8);
padding: 10px;
color: black;
font-size: 10pt;
box-shadow: 2px 2px 16px #777;
}
#flyout h2 {
margin: 8px 4px;
color: black;
vertical-align: middle;
}
#flyout h2 img {
height: 32px;
padding-right: 8px;
vertical-align: middle;
}
#flyout p, table {
margin: 4px 4px;
}
#flyout td.label {
font-style: italic;
color: #777;
padding-right: 12px;
}
#flyout td.value {
}
#flyout hr {
height: 1px;
color: #ccc;
background-color: #ccc;
border: 0;
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Base Framework
*/
(function ($) {
'use strict';
var tsI = new Date().getTime(), // initialize time stamp
tsB, // build time stamp
mastHeight = 36, // see mast2.css
defaultVid = 'sample';
// attach our main function to the jQuery object
$.onos = function (options) {
var uiApi,
viewApi,
navApi,
libApi,
exported = {};
var defaultOptions = {
trace: false,
theme: 'dark',
startVid: defaultVid
};
// compute runtime settings
var settings = $.extend({}, defaultOptions, options);
// set the selected theme
d3.select('body').classed(settings.theme, true);
// internal state
var views = {},
fpanels = {},
current = {
view: null,
ctx: '',
flags: {},
theme: settings.theme
},
built = false,
buildErrors = [],
keyHandler = {
globalKeys: {},
maskedKeys: {},
viewKeys: {},
viewFn: null,
viewGestures: []
},
alerts = {
open: false,
count: 0
};
// DOM elements etc.
// TODO: verify existence of following elements...
var $view = d3.select('#view'),
$floatPanels = d3.select('#floatPanels'),
$alerts = d3.select('#alerts'),
// note, following elements added programmatically...
$mastRadio;
function whatKey(code) {
switch (code) {
case 13: return 'enter';
case 16: return 'shift';
case 17: return 'ctrl';
case 18: return 'alt';
case 27: return 'esc';
case 32: return 'space';
case 37: return 'leftArrow';
case 38: return 'upArrow';
case 39: return 'rightArrow';
case 40: return 'downArrow';
case 91: return 'cmdLeft';
case 93: return 'cmdRight';
case 187: return 'equals';
case 189: return 'dash';
case 191: return 'slash';
case 192: return 'backQuote';
case 220: return 'backSlash';
default:
if ((code >= 48 && code <= 57) ||
(code >= 65 && code <= 90)) {
return String.fromCharCode(code);
} else if (code >= 112 && code <= 123) {
return 'F' + (code - 111);
}
return '.';
}
}
// ..........................................................
// Internal functions
// throw an error
function throwError(msg) {
// separate function, as we might add tracing here too, later
throw new Error(msg);
}
function doError(msg) {
console.error(msg);
doAlert(msg);
}
function trace(msg) {
if (settings.trace) {
console.log(msg);
}
}
function traceFn(fn, params) {
if (settings.trace) {
console.log('*FN* ' + fn + '(...): ' + params);
}
}
// hash navigation
function hash() {
var hash = window.location.hash,
redo = false,
view,
t;
traceFn('hash', hash);
if (!hash) {
hash = settings.startVid;
redo = true;
}
t = parseHash(hash);
if (!t || !t.vid) {
doError('Unable to parse target hash: "' + hash + '"');
}
view = views[t.vid];
if (!view) {
doError('No view defined with id: ' + t.vid);
}
if (redo) {
window.location.hash = makeHash(t);
// the above will result in a hashchange event, invoking
// this function again
} else {
// hash was not modified... navigate to where we need to be
navigate(hash, view, t);
}
}
function parseHash(s) {
// extract navigation coordinates from the supplied string
// "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
traceFn('parseHash', s);
// look for use of flags, first
var vidctx,
vid,
ctx,
flags,
flagMap,
m;
// RE that includes flags ('?flag1,flag2')
m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
if (m) {
vidctx = m[1];
flags = m[2];
flagMap = {};
} else {
// no flags
m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
if (m) {
vidctx = m[1];
} else {
// bad hash
return null;
}
}
vidctx = vidctx.split(',');
vid = vidctx[0];
ctx = vidctx[1];
if (flags) {
flags.split(',').forEach(function (f) {
flagMap[f.trim()] = true;
});
}
return {
vid: vid.trim(),
ctx: ctx ? ctx.trim() : '',
flags: flagMap
};
}
function makeHash(t, ctx, flags) {
traceFn('makeHash');
// make a hash string from the given navigation coordinates,
// and optional flags map.
// if t is not an object, then it is a vid
var h = t,
c = ctx || '',
f = $.isPlainObject(flags) ? flags : null;
if ($.isPlainObject(t)) {
h = t.vid;
c = t.ctx || '';
f = t.flags || null;
}
if (c) {
h += ',' + c;
}
if (f) {
h += '?' + d3.map(f).keys().join(',');
}
trace('hash = "' + h + '"');
return h;
}
function navigate(hash, view, t) {
traceFn('navigate', view.vid);
// closePanes() // flyouts etc.
// updateNav() // accordion / selected nav item etc.
createView(view);
setView(view, hash, t);
}
function buildError(msg) {
buildErrors.push(msg);
}
function reportBuildErrors() {
traceFn('reportBuildErrors');
var nerr = buildErrors.length,
errmsg;
if (!nerr) {
console.log('(no build errors)');
} else {
errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
buildErrors.join('\n');
doAlert(errmsg);
console.error(errmsg);
}
}
// returns the reference if it is a function, null otherwise
function isF(f) {
return $.isFunction(f) ? f : null;
}
// returns the reference if it is an array, null otherwise
function isA(a) {
return $.isArray(a) ? a : null;
}
// ..........................................................
// View life-cycle functions
function setViewDimensions(sel) {
var w = window.innerWidth,
h = window.innerHeight - mastHeight;
sel.each(function () {
$(this)
.css('width', w + 'px')
.css('height', h + 'px')
});
}
function createView(view) {
var $d;
// lazy initialization of the view
if (view && !view.$div) {
trace('creating view for ' + view.vid);
$d = $view.append('div')
.attr({
id: view.vid,
class: 'onosView'
});
setViewDimensions($d);
view.$div = $d; // cache a reference to the D3 selection
}
}
function setView(view, hash, t) {
traceFn('setView', view.vid);
// set the specified view as current, while invoking the
// appropriate life-cycle callbacks
// first, we'll start by closing the alerts pane, if open
closeAlerts();
// if there is a current view, and it is not the same as
// the incoming view, then unload it...
if (current.view && (current.view.vid !== view.vid)) {
current.view.unload();
// detach radio buttons, key handlers, etc.
$('#mastRadio').children().detach();
keyHandler.viewKeys = {};
keyHandler.viewFn = null;
}
// cache new view and context
current.view = view;
current.ctx = t.ctx || '';
current.flags = t.flags || {};
// init is called only once, after the view is in the DOM
if (!view.inited) {
view.init(current.ctx, current.flags);
view.inited = true;
}
// clear the view of stale data
view.reset();
// load the view
view.load(current.ctx, current.flags);
}
// generate 'unique' id by prefixing view id
function makeUid(view, id) {
return view.vid + '-' + id;
}
// restore id by removing view id prefix
function unmakeUid(view, uid) {
var re = new RegExp('^' + view.vid + '-');
return uid.replace(re, '');
}
function setRadioButtons(vid, btnSet) {
var view = views[vid],
btnG,
api = {};
// lazily create the buttons...
if (!(btnG = view.radioButtons)) {
btnG = d3.select(document.createElement('div'));
btnG.buttonDef = {};
btnSet.forEach(function (btn, i) {
var bid = btn.id || 'b' + i,
txt = btn.text || 'Button #' + i,
uid = makeUid(view, bid),
button = btnG.append('span')
.attr({
id: uid,
class: 'radio'
})
.text(txt);
btn.id = bid;
btnG.buttonDef[uid] = btn;
if (i === 0) {
button.classed('active', true);
btnG.selected = bid;
}
});
btnG.selectAll('span')
.on('click', function (d) {
var button = d3.select(this),
uid = button.attr('id'),
btn = btnG.buttonDef[uid],
act = button.classed('active');
if (!act) {
btnG.selectAll('span').classed('active', false);
button.classed('active', true);
btnG.selected = btn.id;
if (isF(btn.cb)) {
btn.cb(view.token(), btn);
}
}
});
view.radioButtons = btnG;
api.selected = function () {
return btnG.selected;
}
}
// attach the buttons to the masthead
$mastRadio.node().appendChild(btnG.node());
// return an api for interacting with the button set
return api;
}
function setupGlobalKeys() {
$.extend(keyHandler, {
globalKeys: {
backSlash: [quickHelp, 'Show / hide Quick Help'],
slash: [quickHelp, 'Show / hide Quick Help'],
esc: [escapeKey, 'Dismiss dialog or cancel selections'],
T: [toggleTheme, "Toggle theme"]
},
globalFormat: ['backSlash', 'slash', 'esc', 'T'],
// Masked keys are global key handlers that always return true.
// That is, the view will never see the event for that key.
maskedKeys: {
slash: true,
backSlash: true,
T: true
}
});
}
function quickHelp(view, key, code, ev) {
libApi.quickHelp.show(keyHandler);
return true;
}
function escapeKey(view, key, code, ev) {
if (alerts.open) {
closeAlerts();
return true;
}
if (libApi.quickHelp.hide()) {
return true;
}
return false;
}
function toggleTheme(view, key, code, ev) {
var body = d3.select('body');
current.theme = (current.theme === 'light') ? 'dark' : 'light';
body.classed('light dark', false);
body.classed(current.theme, true);
theme(view);
return true;
}
function setGestureNotes(g) {
keyHandler.viewGestures = isA(g) || [];
}
function setKeyBindings(keyArg) {
var viewKeys,
masked = [];
if ($.isFunction(keyArg)) {
// set general key handler callback
keyHandler.viewFn = keyArg;
} else {
// set specific key filter map
viewKeys = d3.map(keyArg).keys();
viewKeys.forEach(function (key) {
if (keyHandler.maskedKeys[key]) {
masked.push(' Key "' + key + '" is reserved');
}
});
if (masked.length) {
doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
}
keyHandler.viewKeys = keyArg;
}
}
function keyIn() {
var event = d3.event,
keyCode = event.keyCode,
key = whatKey(keyCode),
kh = keyHandler,
gk = kh.globalKeys[key],
gcb = isF(gk) || (isA(gk) && isF(gk[0])),
vk = kh.viewKeys[key],
vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(kh.viewFn),
token = current.view.token();
// global callback?
if (gcb && gcb(token, key, keyCode, event)) {
// if the event was 'handled', we are done
return;
}
// otherwise, let the view callback have a shot
if (vcb) {
vcb(token, key, keyCode, event);
}
}
function createAlerts() {
$alerts.style('display', 'block');
$alerts.append('span')
.attr('class', 'close')
.text('X')
.on('click', closeAlerts);
$alerts.append('pre');
$alerts.append('p').attr('class', 'footnote')
.text('Press ESCAPE to close');
alerts.open = true;
alerts.count = 0;
}
function closeAlerts() {
$alerts.style('display', 'none')
.html('');
alerts.open = false;
}
function addAlert(msg) {
var lines,
oldContent;
if (alerts.count) {
oldContent = $alerts.select('pre').html();
}
lines = msg.split('\n');
lines[0] += ' '; // spacing so we don't crowd 'X'
lines = lines.join('\n');
if (oldContent) {
lines += '\n----\n' + oldContent;
}
$alerts.select('pre').html(lines);
alerts.count++;
}
function doAlert(msg) {
if (!alerts.open) {
createAlerts();
}
addAlert(msg);
}
function resize(e) {
d3.selectAll('.onosView').call(setViewDimensions);
// allow current view to react to resize event...
if (current.view) {
current.view.resize(current.ctx, current.flags);
}
}
function theme() {
// allow current view to react to theme event...
if (current.view) {
current.view.theme(current.ctx, current.flags);
}
}
// ..........................................................
// View class
// Captures state information about a view.
// Constructor
// vid : view id
// nid : id of associated nav-item (optional)
// cb : callbacks (init, reset, load, unload, resize, theme, error)
function View(vid) {
var av = 'addView(): ',
args = Array.prototype.slice.call(arguments),
nid,
cb;
args.shift(); // first arg is always vid
if (typeof args[0] === 'string') { // nid specified
nid = args.shift();
}
cb = args.shift();
this.vid = vid;
if (validateViewArgs(vid)) {
this.nid = nid; // explicit navitem id (can be null)
this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
this.$div = null; // view not yet added to DOM
this.radioButtons = null; // no radio buttons yet
this.ok = true; // valid view
}
}
function validateViewArgs(vid) {
var av = "ui.addView(...): ",
ok = false;
if (typeof vid !== 'string' || !vid) {
doError(av + 'vid required');
} else if (views[vid]) {
doError(av + 'View ID "' + vid + '" already exists');
} else {
ok = true;
}
return ok;
}
var viewInstanceMethods = {
token: function () {
return {
// attributes
vid: this.vid,
nid: this.nid,
$div: this.$div,
// functions
width: this.width,
height: this.height,
uid: this.uid,
setRadio: this.setRadio,
setKeys: this.setKeys,
setGestures: this.setGestures,
dataLoadError: this.dataLoadError,
alert: this.alert,
flash: this.flash,
getTheme: this.getTheme
}
},
// == Life-cycle functions
// TODO: factor common code out of life-cycle
init: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.init);
traceFn('View.init', this.vid + ', ' + c);
if (fn) {
trace('INIT cb for ' + this.vid);
fn(this.token(), c, flags);
}
},
reset: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.reset);
traceFn('View.reset', this.vid);
if (fn) {
trace('RESET cb for ' + this.vid);
fn(this.token(), c, flags);
} else if (this.cb.reset === true) {
// boolean true signifies "clear view"
trace(' [true] cleaing view...');
viewApi.empty();
}
},
load: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.load);
traceFn('View.load', this.vid + ', ' + c);
this.$div.classed('currentView', true);
if (fn) {
trace('LOAD cb for ' + this.vid);
fn(this.token(), c, flags);
}
},
unload: function (ctx, flags) {
var c = ctx | '',
fn = isF(this.cb.unload);
traceFn('View.unload', this.vid);
this.$div.classed('currentView', false);
if (fn) {
trace('UNLOAD cb for ' + this.vid);
fn(this.token(), c, flags);
}
},
resize: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.resize),
w = this.width(),
h = this.height();
traceFn('View.resize', this.vid + '/' + c +
' [' + w + 'x' + h + ']');
if (fn) {
trace('RESIZE cb for ' + this.vid);
fn(this.token(), c, flags);
}
},
theme: function (ctx, flags) {
var c = ctx | '',
fn = isF(this.cb.theme);
traceFn('View.theme', this.vid);
if (fn) {
trace('THEME cb for ' + this.vid);
fn(this.token(), c, flags);
}
},
error: function (ctx, flags) {
var c = ctx || '',
fn = isF(this.cb.error);
traceFn('View.error', this.vid + ', ' + c);
if (fn) {
trace('ERROR cb for ' + this.vid);
fn(this.token(), c, flags);
}
},
// == Token API functions
width: function () {
return $(this.$div.node()).width();
},
height: function () {
return $(this.$div.node()).height();
},
setRadio: function (btnSet) {
return setRadioButtons(this.vid, btnSet);
},
setKeys: function (keyArg) {
setKeyBindings(keyArg);
},
setGestures: function (g) {
setGestureNotes(g);
},
getTheme: function () {
return current.theme;
},
uid: function (id) {
return makeUid(this, id);
},
// TODO : add exportApi and importApi methods
// TODO : implement custom dialogs
// Consider enhancing alert mechanism to handle multiples
// as individually closable.
alert: function (msg) {
doAlert(msg);
},
flash: function (msg) {
libApi.feedback.flash(msg);
},
dataLoadError: function (err, url) {
var msg = 'Data Load Error\n\n' +
err.status + ' -- ' + err.statusText + '\n\n' +
'relative-url: "' + url + '"\n\n' +
'complete-url: "' + err.responseURL + '"';
this.alert(msg);
}
// TODO: consider schedule, clearTimer, etc.
};
// attach instance methods to the view prototype
$.extend(View.prototype, viewInstanceMethods);
// ..........................................................
// UI API
var fpConfig = {
TR: {
side: 'right'
},
TL: {
side: 'left'
}
};
uiApi = {
addLib: function (libName, api) {
// TODO: validation of args
libApi[libName] = api;
},
// TODO: implement floating panel as a class
// TODO: parameterize position (currently hard-coded to TopRight)
/*
* Creates div in floating panels block, with the given id.
* Returns panel token used to interact with the panel
*/
addFloatingPanel: function (id, position) {
var pos = position || 'TR',
cfg = fpConfig[pos],
el,
fp,
on = false;
if (fpanels[id]) {
buildError('Float panel with id "' + id + '" already exists.');
return null;
}
el = $floatPanels.append('div')
.attr('id', id)
.attr('class', 'fpanel')
.style('opacity', 0);
// has to be called after el is set.
el.style(cfg.side, pxHide());
function pxShow() {
return '20px';
}
function pxHide() {
return (-20 - widthVal()) + 'px';
}
function noPx(what) {
return el.style(what).replace(/px$/, '');
}
function widthVal() {
return noPx('width');
}
function heightVal() {
return noPx('height');
}
function noop() {}
fp = {
id: id,
el: el,
pos: pos,
isVisible: function () {
return on;
},
show: function (cb) {
var endCb = isF(cb) || noop;
on = true;
el.transition().duration(750)
.each('end', endCb)
.style(cfg.side, pxShow())
.style('opacity', 1);
},
hide: function (cb) {
var endCb = isF(cb) || noop;
on = false;
el.transition().duration(750)
.each('end', endCb)
.style(cfg.side, pxHide())
.style('opacity', 0);
},
empty: function () {
return el.html('');
},
append: function (what) {
return el.append(what);
},
width: function (w) {
if (w === undefined) {
return widthVal();
}
el.style('width', w + 'px');
},
height: function (h) {
if (h === undefined) {
return heightVal();
}
el.style('height', h + 'px');
}
};
fpanels[id] = fp;
return fp;
},
// TODO: it remains to be seen whether we keep this style of docs
/** @api ui addView( vid, nid, cb )
* Adds a view to the UI.
* <p>
* Views are loaded/unloaded into the view content pane at
* appropriate times, by the navigation framework. This method
* adds a view to the UI and returns a token object representing
* the view. A view's token is always passed as the first
* argument to each of the view's life-cycle callback functions.
* <p>
* Note that if the view is directly referenced by a nav-item,
* or in a group of views with one of those views referenced by
* a nav-item, then the <i>nid</i> argument can be omitted as
* the framework can infer it.
* <p>
* <i>cb</i> is a plain object containing callback functions:
* "init", "reset", "load", "unload", "resize", "theme", "error".
* <pre>
* function myLoad(view, ctx) { ... }
* ...
* // short form...
* onos.ui.addView('viewId', {
* load: myLoad
* });
* </pre>
*
* @param vid (string) [*] view ID (a unique DOM element id)
* @param nid (string) nav-item ID (a unique DOM element id)
* @param cb (object) [*] callbacks object
* @return the view token
*/
addView: function (vid, nid, cb) {
traceFn('addView', vid);
var view = new View(vid, nid, cb),
token;
if (view.ok) {
views[vid] = view;
token = view.token();
} else {
token = { vid: view.vid, bad: true };
}
return token;
}
};
// ..........................................................
// View API
// TODO: deprecated
viewApi = {
/** @api view empty( )
* Empties the current view.
* <p>
* More specifically, removes all DOM elements from the
* current view's display div.
*/
empty: function () {
if (!current.view) {
return;
}
current.view.$div.html('');
}
};
// ..........................................................
// Nav API
navApi = {
};
// ..........................................................
// Library API
libApi = {
};
// ..........................................................
// Exported API
// function to be called from index.html to build the ONOS UI
function buildOnosUi() {
tsB = new Date().getTime();
tsI = tsB - tsI; // initialization duration
console.log('ONOS UI initialized in ' + tsI + 'ms');
if (built) {
throwError("ONOS UI already built!");
}
built = true;
$mastRadio = d3.select('#mastRadio');
$(window).on('hashchange', hash);
$(window).on('resize', resize);
d3.select('body').on('keydown', keyIn);
setupGlobalKeys();
// Invoke hashchange callback to navigate to content
// indicated by the window location hash.
hash();
// If there were any build errors, report them
reportBuildErrors();
}
// export the api and build-UI function
return {
ui: uiApi,
lib: libApi,
//view: viewApi,
nav: navApi,
buildUi: buildOnosUi,
exported: exported
};
};
}(jQuery));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Feedback layer -- CSS file
*/
#feedback {
z-index: 1400;
}
#feedback svg {
position: absolute;
bottom: 0;
opacity: 0.8;
}
#feedback svg g.feedbackItem {
background-color: teal;
}
#feedback svg g.feedbackItem rect {
fill: #ccc;
}
#feedback svg g.feedbackItem text {
fill: #333;
stroke: none;
text-anchor: middle;
alignment-baseline: middle;
font-size: 16pt;
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Feedback layer
Defines the feedback layer for the UI. Used to give user visual feedback
of choices, typically responding to keystrokes.
*/
(function (onos){
'use strict';
// API's
var api = onos.api;
// Config variables
var w = '100%',
h = 200,
fade = 200,
showFor = 1200,
vb = '-200 -' + (h/2) + ' 400 ' + h,
xpad = 20,
ypad = 10;
// State variables
var timer = null,
data = [];
// DOM elements and the like
var fb = d3.select('#feedback');
var svg;
//var svg = fb.append('svg').attr({
// width: w,
// height: h,
// viewBox: vb
//});
function computeBox(el) {
var text = el.select('text'),
box = text.node().getBBox();
// center
box.x = -box.width / 2;
box.y = -box.height / 2;
// add some padding
box.x -= xpad;
box.width += xpad * 2;
box.y -= ypad;
box.height += ypad * 2;
return box;
}
function updateFeedback() {
if (!svg) {
svg = fb.append('svg').attr({
width: w,
height: h,
viewBox: vb
});
}
var items = svg.selectAll('.feedbackItem')
.data(data);
var entering = items.enter()
.append('g')
.attr({
class: 'feedbackItem',
opacity: 0
})
.transition()
.duration(fade)
.attr('opacity', 1);
entering.each(function (d) {
var el = d3.select(this),
box;
d.el = el;
el.append('rect').attr({ rx: 10, ry: 10});
el.append('text').text(d.label);
box = computeBox(el);
el.select('rect').attr(box);
});
items.exit()
.transition()
.duration(fade)
.attr({ opacity: 0})
.remove();
if (svg && data.length === 0) {
svg.transition()
.delay(fade + 10)
.remove();
svg = null;
}
}
function clearFlash() {
if (timer) {
clearInterval(timer);
}
data = [];
updateFeedback();
}
// for now, simply display some text feedback
function flash(text) {
// cancel old scheduled event if there was one
if (timer) {
clearInterval(timer);
}
timer = setInterval(function () {
clearFlash();
}, showFor);
data = [{
label: text
}];
updateFeedback();
}
onos.ui.addLib('feedback', {
flash: flash
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Floating Panels -- CSS file
*/
.fpanel {
position: absolute;
z-index: 100;
display: block;
top: 64px;
width: 260px;
right: -300px;
opacity: 0;
background-color: rgba(255,255,255,0.8);
padding: 10px;
color: black;
font-size: 10pt;
-moz-border-radius: 6px;
border-radius: 6px;
box-shadow: 0px 2px 12px #777;
}
/* TODO: light/dark themes */
.light .fpanel {
}
.dark .fpanel {
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Masthead -- CSS file
*/
#mast {
height: 36px;
padding: 4px;
vertical-align: baseline;
}
.light #mast {
background-color: #bbb;
box-shadow: 0 2px 8px #777;
}
.dark #mast {
background-color: #444;
box-shadow: 0 2px 8px #777;
}
#mast img#logo {
height: 38px;
padding-left: 8px;
padding-right: 8px;
}
#mast span.title {
font-size: 14pt;
font-style: italic;
vertical-align: 12px;
}
.light #mast span.title {
color: #369;
}
.dark #mast span.title {
color: #eee;
}
#mast span.right {
padding-top: 8px;
padding-right: 16px;
float: right;
}
#mast span.radio {
font-size: 10pt;
margin: 4px 2px;
padding: 1px 6px;
-moz-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
}
.light #mast span.radio {
border: 1px dotted #222;
color: #eee;
}
.dark #mast span.radio {
border: 1px dotted #bbb;
color: #888;
}
#mast span.radio.active {
padding: 1px 6px;
}
.light #mast span.radio.active {
background-color: #bbb;
border: 1px solid #eee;
color: #666;
}
.dark #mast span.radio.active {
background-color: #222;
border: 1px solid #eee;
color: #78a;
}
/* Button Bar */
#bb {
margin: 0 30px;
padding: 0 2px;
}
#bb .btn {
margin: 0 4px;
padding: 2px 6px;
-moz-border-radius: 3px;
border-radius: 3px;
font-size: 9pt;
cursor: pointer;
}
.light #bb .btn {
border: 1px solid #fff;
border-right-color: #444;
border-bottom-color: #444;
color: #222;
}
.light #bb .btn.active {
border: 1px solid #444;
border-right-color: #fff;
border-bottom-color: #fff;
background-color: #888;
color: #ddf;
}
.dark #bb .btn {
border: 1px solid #888;
border-right-color: #111;
border-bottom-color: #111;
color: #888;
}
.dark #bb .btn.active {
border: 1px solid #111;
border-right-color: #888;
border-bottom-color: #888;
background-color: #555;
color: #78a;
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Masthead script
Defines the masthead for the UI. Injects logo and title, as well as providing
the placeholder for a set of radio buttons.
*/
(function (onos){
'use strict';
// API's
var api = onos.api;
// Config variables
var guiTitle = 'Open Network Operating System';
// DOM elements and the like
var mast = d3.select('#mast');
mast.append('img')
.attr({
id: 'logo',
src: 'onos-logo.png'
});
mast.append('span')
.attr({
class: 'title'
})
.text(guiTitle);
mast.append('span')
.attr({
id: 'mastRadio',
class: 'right'
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Quick Help layer -- CSS file
*/
#quickhelp {
z-index: 1300;
}
#quickhelp svg {
position: absolute;
top: 180px;
opacity: 1;
}
#quickhelp svg g.help rect {
fill: black;
opacity: 0.7;
}
#quickhelp svg text.title {
font-size: 10pt;
font-style: italic;
text-anchor: middle;
fill: #999;
}
#quickhelp svg g.keyItem {
fill: white;
}
#quickhelp svg g line.qhrowsep {
stroke: #888;
stroke-dasharray: 2 2;
}
#quickhelp svg text {
font-size: 7pt;
alignment-baseline: middle;
}
#quickhelp svg text.key {
fill: #add;
}
#quickhelp svg text.desc {
fill: #ddd;
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Quick Help Layer
Defines the key-map layer for the UI. Used to give user a list of
key bindings; both global, and for the current view.
*/
(function (onos){
'use strict';
// Config variables
var w = '100%',
h = '80%',
fade = 500,
vb = '-200 0 400 400';
// layout configuration
var pad = 10,
offy = 45,
sepYDelta = 20,
colXDelta = 16,
yTextSpc = 12,
offDesc = 8;
// State variables
var data = [],
yCount;
// DOM elements and the like
var qhdiv = d3.select('#quickhelp'),
svg = qhdiv.select('svg'),
pane, rect, items;
// General functions
function isA(a) { return $.isArray(a) ? a : null; }
function isS(s) { return typeof s === 'string'; }
function cap(s) {
return s.replace(/^[a-z]/, function (m) { return m.toUpperCase(); });
}
var keyDisp = {
equals: '=',
dash: '-',
slash: '/',
backSlash: '\\',
backQuote: '`',
leftArrow: 'L-arrow',
upArrow: 'U-arrow',
rightArrow: 'R-arrow',
downArrow: 'D-arrow'
};
function mkKeyDisp(id) {
var v = keyDisp[id] || id;
return cap(v);
}
function addSeparator(el, i) {
var y = sepYDelta/2 - 5;
el.append('line')
.attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y });
}
function addContent(el, data, ri) {
var xCount = 0,
clsPfx = 'qh-r' + ri + '-c';
function addColumn(el, c, i) {
var cls = clsPfx + i,
oy = 0,
aggKey = el.append('g').attr('visibility', 'hidden'),
gcol = el.append('g').attr({
'class': cls,
transform: translate(xCount, 0)
});
c.forEach(function (j) {
var k = j[0],
v = j[1];
if (k !== '-') {
aggKey.append('text').text(k);
gcol.append('text').text(k)
.attr({
'class': 'key',
y: oy
});
gcol.append('text').text(v)
.attr({
'class': 'desc',
y: oy
});
}
oy += yTextSpc;
});
// adjust position of descriptions, based on widest key
var kbox = aggKey.node().getBBox(),
ox = kbox.width + offDesc;
gcol.selectAll('.desc').attr('x', ox);
aggKey.remove();
// now update x-offset for next column
var bbox = gcol.node().getBBox();
xCount += bbox.width + colXDelta;
}
data.forEach(function (d, i) {
addColumn(el, d, i);
});
// finally, return the height of the row..
return el.node().getBBox().height;
}
function updateKeyItems() {
var rows = items.selectAll('.qhRow').data(data);
yCount = offy;
var entering = rows.enter()
.append('g')
.attr({
'class': 'qhrow'
});
entering.each(function (r, i) {
var el = d3.select(this),
sep = r.type === 'sep',
dy;
el.attr('transform', translate(0, yCount));
if (sep) {
addSeparator(el, i);
yCount += sepYDelta;
} else {
dy = addContent(el, r.data, i);
yCount += dy;
}
});
// size the backing rectangle
var ibox = items.node().getBBox(),
paneW = ibox.width + pad * 2,
paneH = ibox.height + offy;
items.selectAll('.qhrowsep').attr('x2', ibox.width);
items.attr('transform', translate(-paneW/2, -pad));
rect.attr({
width: paneW,
height: paneH,
transform: translate(-paneW/2-pad, 0)
});
}
function translate(x, y) {
return 'translate(' + x + ',' + y + ')';
}
function checkFmt(fmt) {
// should be a single array of keys,
// or array of arrays of keys (one per column).
// return null if there is a problem.
var a = isA(fmt),
n = a && a.length,
ns = 0,
na = 0;
if (n) {
// it is an array which has some content
a.forEach(function (d) {
isA(d) && na++;
isS(d) && ns++;
});
if (na === n || ns === n) {
// all arrays or all strings...
return a;
}
}
return null;
}
function buildBlock(map, fmt) {
var b = [];
fmt.forEach(function (k) {
var v = map.get(k),
a = isA(v),
d = (a && a[1]);
// '-' marks a separator; d is the description
if (k === '-' || d) {
b.push([mkKeyDisp(k), d]);
}
});
return b;
}
function emptyRow() {
return { type: 'row', data: []};
}
function mkArrRow(fmt) {
var d = emptyRow();
d.data.push(fmt);
return d;
}
function mkColumnarRow(map, fmt) {
var d = emptyRow();
fmt.forEach(function (a) {
d.data.push(buildBlock(map, a));
});
return d;
}
function mkMapRow(map, fmt) {
var d = emptyRow();
d.data.push(buildBlock(map, fmt));
return d;
}
function addRow(row) {
var d = row || { type: 'sep' };
data.push(d);
}
function aggregateData(bindings) {
var hf = '_helpFormat',
gmap = d3.map(bindings.globalKeys),
gfmt = bindings.globalFormat,
vmap = d3.map(bindings.viewKeys),
vgest = bindings.viewGestures,
vfmt, vkeys;
// filter out help format entry
vfmt = checkFmt(vmap.get(hf));
vmap.remove(hf);
// if bad (or no) format, fallback to sorted keys
if (!vfmt) {
vkeys = vmap.keys();
vfmt = vkeys.sort();
}
data = [];
addRow(mkMapRow(gmap, gfmt));
addRow();
addRow(isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt));
addRow();
addRow(mkArrRow(vgest));
}
function popBind(bindings) {
pane = svg.append('g')
.attr({
class: 'help',
opacity: 0
});
rect = pane.append('rect')
.attr('rx', 8);
pane.append('text')
.text('Quick Help')
.attr({
class: 'title',
dy: '1.2em',
transform: translate(-pad,0)
});
items = pane.append('g');
aggregateData(bindings);
updateKeyItems();
_fade(1);
}
function fadeBindings() {
_fade(0);
}
function _fade(o) {
svg.selectAll('g.help')
.transition()
.duration(fade)
.attr('opacity', o);
}
function addSvg() {
svg = qhdiv.append('svg')
.attr({
width: w,
height: h,
viewBox: vb
});
}
function removeSvg() {
svg.transition()
.delay(fade + 20)
.remove();
}
function showQuickHelp(bindings) {
svg = qhdiv.select('svg');
if (svg.empty()) {
addSvg();
popBind(bindings);
} else {
hideQuickHelp();
}
}
function hideQuickHelp() {
svg = qhdiv.select('svg');
if (!svg.empty()) {
fadeBindings();
removeSvg();
return true;
}
return false;
}
onos.ui.addLib('quickHelp', {
show: showQuickHelp,
hide: hideQuickHelp
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Sample module file to illustrate framework integration.
*/
(function (onos) {
'use strict';
var pi = Math.PI,
svg,
dotG,
nCircles = 12,
circleData = [],
dotId = 0,
angle = 360 / nCircles,
baseAngle = -90 - angle,
groupRadius = 120,
dotRadius = 24,
dotMoveMs = 800,
dotAppearMs = 300,
dotEase = 'elastic',
colorScale = d3.scale.linear()
.domain([-pi/2, 2*pi/4, 3*pi/2])
.range(['green', 'goldenrod', 'blue']);
// set the size of the SVG layer to match that of the view
function sizeSvg(view) {
svg.attr({
width: view.width(),
height: view.height()
});
}
// gets invoked only the first time the view is loaded
function init(view, ctx, flags) {
// prepare our SVG layer...
svg = view.$div.append('svg');
sizeSvg(view);
dotG = svg.append('g').attr('id', 'dots');
}
// gets invoked just before our view is loaded
function reset(view, ctx, flags) {
// clear dot group and reset circle data
dotG.html('');
circleData = [];
// also clear text, if any
svg.selectAll('text').remove();
}
function updateCirclePositions(view, addNew) {
var w = view.width(),
h = view.height(),
ox = w / 2,
oy = h / 2;
// reposition existing dots
circleData.forEach(function (c, i) {
var inc = addNew ? 1 : 0,
theta = ((i + inc) * angle + baseAngle) * pi/180,
dx = Math.cos(theta) * groupRadius,
dy = Math.sin(theta) * groupRadius,
x = ox + dx,
y = oy + dy;
if (!addNew && i === 0) {
x = ox;
y = oy;
}
c.cx = x;
c.cy = y;
c.rgb = colorScale(theta);
});
if (addNew) {
// introduce a new dot
circleData.unshift({
cx: ox,
cy: oy,
id: dotId++
});
}
// +1 to account for the circle in the center..
if (circleData.length > nCircles + 1) {
circleData.splice(nCircles + 1, 1);
}
}
function doCircles(view) {
var ox = view.width() / 2,
oy = view.height() / 2,
stroke = 'black',
fill = 'red',
hoverFill = 'magenta';
// move existing circles, and add a new one
updateCirclePositions(view, true);
var circ = dotG.selectAll('circle')
.data(circleData, function (d) { return d.id; });
// operate on existing elements
circ.on('mouseover', null)
.on('mouseout', null)
.on('click', null)
.transition()
.duration(dotMoveMs)
.ease(dotEase)
.attr({
cx: function (d) { return d.cx; },
cy: function (d) { return d.cy; }
})
.style({
cursor: 'default',
fill: function (d) { return d.rgb; }
});
// operate on entering elements
circ.enter()
.append('circle')
.attr({
cx: function (d) { return d.cx; },
cy: function (d) { return d.cy; },
r: 0
})
.style({
fill: fill,
stroke: stroke,
'stroke-width': 3.5,
cursor: 'pointer',
opacity: 0
})
.on('mouseover', function (d) {
d3.select(this).style('fill', hoverFill);
})
.on('mouseout', function (d) {
d3.select(this).style('fill', fill);
})
.on('click', function (d) {
setTimeout(function() {
doCircles(view, true);
}, 10);
})
.transition()
.delay(dotMoveMs)
.duration(dotAppearMs)
.attr('r', dotRadius)
.style('opacity', 1);
// operate on exiting elements
circ.exit()
.transition()
.duration(750)
.style('opacity', 0)
.attr({
cx: ox,
cy: oy,
r: groupRadius - dotRadius
})
.remove();
}
function load(view, ctx, flags) {
var ctxText = ctx ? 'Context is "' + ctx + '"' : '';
// display our view context
if (ctxText) {
svg.append('text')
.text(ctxText)
.attr({
x: 20,
y: '1.5em'
})
.style({
fill: 'darkgreen',
'font-size': '20pt'
});
}
doCircles(view);
}
function resize(view, ctx, flags) {
sizeSvg(view);
updateCirclePositions(view);
// move exiting dots into new positions, relative to view size
var circ = dotG.selectAll('circle')
.data(circleData, function (d) { return d.id; });
circ.attr({
cx: function (d) { return d.cx; },
cy: function (d) { return d.cy; }
});
}
// == register our view here, with links to lifecycle callbacks
onos.ui.addView('sample', {
init: init,
reset: reset,
load: load,
resize: resize
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Sample view to illustrate hash formats.
*/
(function (onos) {
'use strict';
var intro = "Try using the following hashes in the address bar:",
hashPrefix = '#sampleHash',
suffixes = [
'',
',one',
',two',
',context,ignored',
',context,ignored?a,b,c',
',two?foo',
',three?foo,bar'
],
$d;
function note(txt) {
$d.append('p')
.text(txt)
.style({
'font-size': '10pt',
color: 'darkorange',
padding: '0 20px',
margin: 0
});
}
function para(txt, color) {
var c = color || 'black';
$d.append('p')
.text(txt)
.style({
padding: '2px 8px',
color: c
});
}
function load(view, ctx, flags) {
var c = ctx || '(undefined)',
f = flags ? d3.map(flags).keys() : [];
$d = view.$div;
para(intro);
suffixes.forEach(function (s) {
note(hashPrefix + s);
});
para('View ID: ' + view.vid, 'blue');
para('Context: ' + c, 'blue');
para('Flags: { ' + f.join(', ') + ' }', 'magenta');
}
// == register the view here, with links to lifecycle callbacks
onos.ui.addView('sampleHash', {
reset: true, // empty the div on reset
load: load
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Sample view to illustrate key bindings.
*/
(function (onos) {
'use strict';
var keyDispatch = {
Z: keyUndo,
X: keyCut,
C: keyCopy,
V: keyPaste,
space: keySpace
};
function keyUndo(view) {
note(view, 'Z = UNDO');
}
function keyCut(view) {
note(view, 'X = CUT');
}
function keyCopy(view) {
note(view, 'C = COPY');
}
function keyPaste(view) {
note(view, 'V = PASTE');
}
function keySpace(view) {
note(view, 'The SpaceBar');
}
function note(view, msg) {
view.$div.append('p')
.text(msg)
.style({
'font-size': '10pt',
color: 'darkorange',
padding: '0 20px',
margin: 0
});
}
function keyCallback(view, key, keyCode, event) {
note(view, 'Key = ' + key + ' KeyCode = ' + keyCode);
}
function load(view, ctx) {
// this maps specific keys to specific functions (1)
view.setKeys(keyDispatch);
// whereas, this installs a general key handler function (2)
view.setKeys(keyCallback);
// Note that (1) takes precedence over (2)
view.$div.append('p')
.text('Press a key or two (try Z,X,C,V and others) ...')
.style('padding', '2px 8px');
}
// == register the view here, with links to lifecycle callbacks
onos.ui.addView('sampleKeys', {
reset: true, // empty the div on reset
load: load
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Sample view to illustrate radio buttons.
*/
(function (onos) {
'use strict';
var intro = [ 'Yo, radio button set...', 'Time to shine' ],
btnSet = [
{ text: 'First Button', cb: cbRadio },
{ text: 'Second Button', cb: cbRadio },
{ text: 'Third Button', cb: cbRadio }
];
// radio button callback
function cbRadio(view, btn) {
write(view, 'You pressed the ' + btn.text);
}
function write(view, msg) {
view.$div.append('p')
.text(msg)
.style({
'font-size': '10pt',
color: 'green',
padding: '0 20px',
margin: '2px'
});
}
// invoked when the view is loaded
function load(view, ctx) {
view.setRadio(btnSet);
view.$div.selectAll('p')
.data(intro)
.enter()
.append('p')
.text(function (d) { return d; })
.style('padding', '2px 8px');
}
// == register the view here, with links to lifecycle callbacks
onos.ui.addView('sampleRadio', {
reset: true, // empty the div on reset
load: load
});
}(ONOS));
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Topology view -- CSS file
*/
#topo svg #topo-bg {
opacity: 0.5;
}
#topo #map {
stroke-width: 2px;
stroke: #eee;
fill: transparent;
}
/* TODO: move glyphs into framework */
#topo svg .glyphIcon {
fill: black;
stroke: none;
fill-rule: evenodd;
}
#topo svg .glyphIcon rect {
fill: #ddd;
stroke: none;
}
#topo svg .noDevsLayer {
visibility: hidden;
}
#topo svg .noDevsLayer text {
font-size: 60pt;
font-style: italic;
fill: #dde;
}
#topo svg .noDevsBird {
fill: #ecd;
}
/* NODES */
#topo svg .node {
cursor: pointer;
}
#topo svg .node.selected rect,
#topo svg .node.selected circle {
fill: #f90;
filter: url(#blue-glow);
}
#topo svg .node text {
pointer-events: none;
}
/* Device Nodes */
#topo svg .node.device {
}
#topo svg .node.device rect {
stroke-width: 1.5;
}
#topo svg .node.device.fixed rect {
stroke-width: 1.5;
stroke: #ccc;
}
/* note: device is offline without the 'online' class */
#topo svg .node.device {
fill: #777;
}
#topo svg .node.device.online {
fill: #6e7fa3;
}
/* note: device is offline without the 'online' class */
#topo svg .node.device text {
fill: #bbb;
font: 10pt sans-serif;
}
#topo svg .node.device.online text {
fill: white;
}
#topo svg .node.device .glyphIcon rect {
fill: #aaa;
}
#topo svg .node.device .glyphIcon use {
fill: #777;
}
#topo svg .node.device.selected .glyphIcon rect {
fill: #f90;
}
#topo svg .node.device.online .glyphIcon rect {
fill: #ccc;
}
#topo svg .node.device.online .glyphIcon use {
fill: #000;
}
#topo svg .node.device.online.selected .glyphIcon rect {
fill: #f90;
}
/* Host Nodes */
#topo svg .node.host {
stroke: #000;
}
#topo svg .node.host text {
fill: #846;
stroke: none;
font: 9pt sans-serif;
}
svg .node.host circle {
stroke: #000;
fill: #edb;
}
/* LINKS */
#topo svg .link {
opacity: .9;
}
#topo svg .link.inactive {
opacity: .5;
stroke-dasharray: 8 4;
}
#topo svg .link.secondary {
stroke: rgba(0,153,51,0.5);
stroke-width: 3px;
}
#topo svg .link.primary {
stroke: #ffA300;
stroke-width: 4px;
}
#topo svg .link.animated {
stroke: #ffA300;
}
#topo svg .link.secondary.optical {
stroke: rgba(128,64,255,0.5);
stroke-width: 4px;
}
#topo svg .link.primary.optical {
stroke: #74f;
stroke-width: 6px;
}
#topo svg .link.animated.optical {
stroke: #74f;
stroke-width: 10px;
}
#topo svg .linkLabel rect {
fill: #eee;
stroke: none;
}
#topo svg .linkLabel text {
text-anchor: middle;
stroke: #777;
stroke-width: 0.1;
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: 0 4px;
top: 20px;
left: 50px;
color: black;
}
#topo-summary h3 {
margin: 0 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 */
top: 320px;
}
#topo-detail svg {
display: inline-block;
width: 42px;
height: 42px;
}
#topo-detail svg .glyphIcon {
fill: black;
stroke: none;
fill-rule: evenodd;
}
#topo-detail h2 {
position: absolute;
margin: 0 4px;
top: 20px;
left: 50px;
color: black;
}
#topo-detail h3 {
margin: 0 4px;
top: 20px;
left: 50px;
color: black;
}
#topo-detail p, table {
margin: 4px 4px;
}
#topo-detail td.label {
font-style: italic;
color: #777;
padding-right: 12px;
}
#topo-detail td.value {
}
#topo-detail .actionBtn {
margin: 6px 12px;
padding: 2px 6px;
font-size: 9pt;
cursor: pointer;
width: 200px;
text-align: center;
/* theme specific... */
border: 2px solid #ddd;
border-radius: 4px;
color: #eee;
background: #888;
}
#topo-detail .actionBtn:hover {
/* theme specific... */
border: 2px solid #ddd;
color: #eee;
background: #444;
}
#topo-detail hr {
height: 1px;
color: #ccc;
background-color: #ccc;
border: 0;
}
/* ONOS instance stuff */
#topo-oibox {
height: 100px;
}
#topo-oibox div.onosInst {
display: inline-block;
width: 170px;
height: 85px;
cursor: pointer;
}
#topo-oibox svg rect {
fill: #ccc;
stroke: #aaa;
stroke-width: 3.5;
}
#topo-oibox .online svg rect {
opacity: 1;
fill: #9cf;
stroke: #555;
}
#topo-oibox svg .glyphIcon {
fill: #888;
fill-rule: evenodd;
}
#topo-oibox .online svg .glyphIcon {
fill: #000;
}
#topo-oibox svg .badgeIcon {
fill: #777;
fill-rule: evenodd;
}
#topo-oibox .online svg .badgeIcon {
fill: #fff;
}
#topo-oibox svg text {
text-anchor: middle;
fill: #777;
}
#topo-oibox .online svg text {
fill: #eee;
}
#topo-oibox svg text.instTitle {
font-size: 11pt;
font-weight: bold;
}
#topo-oibox svg text.instLabel {
font-size: 9pt;
font-style: italic;
}
#topo-oibox .onosInst.mastership {
opacity: 0.3;
}
#topo-oibox .onosInst.mastership.affinity {
opacity: 1.0;
}
#topo-oibox .onosInst.mastership.affinity svg rect {
filter: url(#blue-glow);
}
#topo svg .suppressed {
opacity: 0.2;
}
/* Death Mask (starts hidden) */
#topo-mask {
display: none;
position: absolute;
top: 0;
left: 0;
width: 10000px;
height: 8000px;
z-index: 5000;
background-color: rgba(0,0,0,0.75);
padding: 60px;
}
#topo-mask p {
margin: 8px 20px;
color: #ddd;
font-size: 14pt;
font-style: italic;
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS network topology viewer - version 1.1
*/
(function (onos) {
'use strict';
// shorter names for library APIs
var d3u = onos.lib.d3util,
gly = onos.lib.glyphs;
// configuration data
var config = {
useLiveData: true,
fnTrace: true,
debugOn: false,
birdDim: 400,
options: {
showBackground: true
},
webSockUrl: '../ws/topology',
data: {
live: {
jsonUrl: '../rs/topology/graph',
detailPrefix: '../rs/topology/graph/',
detailSuffix: ''
},
fake: {
jsonUrl: 'json/network2.json',
detailPrefix: 'json/',
detailSuffix: '.json'
}
},
labels: {
imgPad: 16,
padLR: 4,
padTB: 3,
marginLR: 3,
marginTB: 2,
port: {
gap: 3,
width: 18,
height: 14
}
},
topo: {
linkBaseColor: '#666',
linkInColor: '#66f',
linkInWidth: 12,
linkOutColor: '#f00',
linkOutWidth: 10
},
icons: {
device: {
dim: 36,
rx: 4,
xoff: -20,
yoff: -18
},
host: {
defaultRadius: 9,
radius: {
endstation: 14,
bgpSpeaker: 14,
router: 14
}
}
},
force: {
note_for_links: 'link.type is used to differentiate',
linkDistance: {
direct: 100,
optical: 120,
hostLink: 3
},
linkStrength: {
direct: 1.0,
optical: 1.0,
hostLink: 1.0
},
note_for_nodes: 'node.class is used to differentiate',
charge: {
device: -8000,
host: -5000
}
},
// see below in creation of viewBox on main svg
logicalSize: 1000
};
// radio buttons
var layerButtons = [
{ text: 'All Layers', id: 'all', cb: showAllLayers },
{ text: 'Packet Only', id: 'pkt', cb: showPacketLayer },
{ text: 'Optical Only', id: 'opt', cb: showOpticalLayer }
],
layerBtnSet,
layerBtnDispatch = {
all: showAllLayers,
pkt: showPacketLayer,
opt: showOpticalLayer
};
// key bindings
var keyDispatch = {
// ==== "development mode" ====
//0: testMe,
//equals: injectStartupEvents,
//dash: injectTestEvent,
O: [toggleSummary, 'Toggle ONOS summary pane'],
I: [toggleInstances, 'Toggle ONOS instances pane'],
D: [toggleDetails, 'Disable / enable details pane'],
H: [toggleHosts, 'Toggle host visibility'],
M: [toggleOffline, 'Toggle offline visibility'],
B: [toggleBg, 'Toggle background image'],
P: togglePorts,
X: [toggleNodeLock, 'Lock / unlock node positions'],
Z: [toggleOblique, 'Toggle oblique view (Experimental)'],
L: [cycleLabels, 'Cycle device labels'],
U: [unpin, 'Unpin node (hover mouse over)'],
R: [resetPanZoom, 'Reset pan / zoom'],
V: [showRelatedIntentsAction, 'Show all related intents'],
rightArrow: [showNextIntentAction, 'Show next related intent'],
leftArrow: [showPrevIntentAction, 'Show previous related intent'],
W: [showSelectedIntentTrafficAction, 'Monitor traffic of selected intent'],
A: [showAllTrafficAction, 'Monitor all traffic'],
F: [showDeviceLinkFlowsAction, 'Show device link flows'],
E: [equalizeMasters, 'Equalize mastership roles'],
esc: handleEscape,
_helpFormat: [
['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ],
['X', 'Z', 'L', 'U', 'R' ],
['V', 'rightArrow', 'leftArrow', 'W', 'A', 'F', '-', 'E' ]
]
};
// mouse gestures
var gestures = [
['click', 'Select the item and show details'],
['shift-click', 'Toggle selection state'],
['drag', 'Reposition (and pin) device / host'],
['cmd-scroll', 'Zoom in / out'],
['cmd-drag', 'Pan']
];
// state variables
var network = {
view: null, // view token reference
nodes: [],
links: [],
lookup: {},
revLinkToKey: {}
},
scenario = {
evDir: 'json/ev/',
evScenario: '/scenario.json',
evPrefix: '/ev_',
evOnos: '_onos.json',
evUi: '_ui.json',
ctx: null,
params: {},
evNumber: 0,
view: null
},
webSock,
sid = 0,
deviceLabelCount = 3,
hostLabelCount = 2,
deviceLabelIndex = 0,
hostLabelIndex = 0,
selections = {},
selectOrder = [],
hovered = null,
summaryPane,
detailPane,
antTimer = null,
guiSuccessor = null,
onosInstances = {},
onosOrder = [],
oiBox,
oiShowMaster = false,
portLabelsOn = false,
cat7 = d3u.cat7(),
colorAffinity = false,
showHosts = false,
showOffline = true,
useDetails = true,
haveDetails = false,
nodeLock = false,
oblique = false;
// constants
var hoverModeNone = 0,
hoverModeAll = 1,
hoverModeFlows = 2,
hoverModeIntents = 3,
hoverMode = hoverModeNone;
// D3 selections
var svg,
panZoomContainer,
noDevices,
bgImg,
topoG,
nodeG,
linkG,
linkLabelG,
node,
link,
linkLabel,
mask;
// the projection for the map background
var geoMapProj;
// the zoom function
var zoom;
// ==============================
// For Debugging / Development
function fnTrace(msg, id) {
if (config.fnTrace) {
console.log('FN: ' + msg + ' [' + id + ']');
}
}
function evTrace(data) {
fnTrace(data.event, data.payload.id);
}
// ==============================
// Key Callbacks
function flash(txt) {
network.view.flash(txt);
}
function testMe(view) {
//view.alert('Theme is ' + view.getTheme());
//view.flash('This is some text');
cat7.testCard(svg);
}
function injectTestEvent(view) {
if (config.useLiveData) { return; }
var sc = scenario,
evn = ++sc.evNumber,
pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
onosUrl = pfx + sc.evOnos,
uiUrl = pfx + sc.evUi,
stack = [
{ url: onosUrl, cb: handleServerEvent },
{ url: uiUrl, cb: handleUiEvent }
];
recurseFetchEvent(stack, evn);
}
function recurseFetchEvent(stack, evn) {
var v = scenario.view,
frame;
if (stack.length === 0) {
v.alert('Oops!\n\nNo event #' + evn + ' found.');
return;
}
frame = stack.shift();
d3.json(frame.url, function (err, data) {
if (err) {
if (err.status === 404) {
// if we didn't find the data, try the next stack frame
recurseFetchEvent(stack, evn);
} else {
v.alert('non-404 error:\n\n' + frame.url + '\n\n' + err);
}
} else {
wsTrace('test', JSON.stringify(data));
frame.cb(data);
}
});
}
function handleUiEvent(data) {
scenario.view.alert('UI Tx: ' + data.event + '\n\n' +
JSON.stringify(data));
}
function injectStartupEvents(view) {
var last = scenario.params.lastAuto || 0;
if (config.useLiveData) { return; }
while (scenario.evNumber < last) {
injectTestEvent(view);
}
}
function toggleBg() {
var vis = bgImg.style('visibility');
bgImg.style('visibility', visVal(vis === 'hidden'));
}
function opacifyBg(b) {
bgImg.transition()
.duration(1000)
.attr('opacity', b ? 1 : 0);
}
function toggleNodeLock() {
nodeLock = !nodeLock;
flash('Node positions ' + (nodeLock ? 'locked' : 'unlocked'))
}
function toggleOblique() {
oblique = !oblique;
if (oblique) {
network.force.stop();
toObliqueView();
} else {
toNormalView();
}
}
function toggleHosts() {
showHosts = !showHosts;
updateHostVisibility();
flash('Hosts ' + visVal(showHosts));
}
function toggleOffline() {
showOffline = !showOffline;
updateOfflineVisibility();
flash('Offline devices ' + visVal(showOffline));
}
function cycleLabels() {
deviceLabelIndex = (deviceLabelIndex === 2)
? 0 : deviceLabelIndex + 1;
network.nodes.forEach(function (d) {
if (d.class === 'device') {
updateDeviceLabel(d);
}
});
}
function togglePorts(view) {
//view.alert('togglePorts() callback')
}
function unpin() {
if (hovered) {
sendUpdateMeta(hovered);
hovered.fixed = false;
hovered.el.classed('fixed', false);
fResume();
}
}
function handleEscape(view) {
if (oiShowMaster) {
cancelAffinity();
} else if (haveDetails) {
deselectAll();
} else if (oiBox.isVisible()) {
hideInstances();
} else if (summaryPane.isVisible()) {
cancelSummary();
stopAntTimer();
} else {
hoverMode = hoverModeNone;
}
}
function showNoDevs(b) {
noDevices.style('visibility', visVal(b));
}
// ==============================
// Oblique view ...
var obview = {
tt: -.7, // x skew y factor
xsk: -35, // x skew angle
ysc: 0.5, // y scale
pad: 50,
time: 1500,
fill: {
pkt: 'rgba(130,130,170,0.3)',
opt: 'rgba(170,130,170,0.3)'
},
id: function (tag) {
return 'obview-' + tag + 'Plane';
},
yt: function (h, dir) {
return h * obview.ysc * dir * 1.1;
},
obXform: function (h, dir) {
var yt = obview.yt(h, dir);
return scale(1, obview.ysc) + translate(0, yt) + skewX(obview.xsk);
},
noXform: function () {
return skewX(0) + translate(0,0) + scale(1,1);
},
xffn: null,
plane: {}
};
function toObliqueView() {
var box = nodeG.node().getBBox(),
ox, oy;
padBox(box, obview.pad);
ox = box.x + box.width / 2;
oy = box.y + box.height / 2;
// remember node lock state, then lock the nodes down
obview.nodeLock = nodeLock;
nodeLock = true;
opacifyBg(false);
insertPlanes(ox, oy);
obview.xffn = function (xy, dir) {
var yt = obview.yt(box.height, dir),
ax = xy.x - ox,
ay = xy.y - oy,
x = ax + ay * obview.tt,
y = ay * obview.ysc + obview.ysc * yt;
return {x: ox + x, y: oy + y};
};
showPlane('pkt', box, -1);
showPlane('opt', box, 1);
obTransitionNodes();
}
function toNormalView() {
obview.xffn = null;
hidePlane('pkt');
hidePlane('opt');
obTransitionNodes();
removePlanes();
// restore node lock state
nodeLock = obview.nodeLock;
opacifyBg(true);
}
function obTransitionNodes() {
var xffn = obview.xffn;
// return the direction for the node
// -1 for pkt layer, 1 for optical layer
function dir(d) {
return inLayer(d, 'pkt') ? -1 : 1;
}
if (xffn) {
network.nodes.forEach(function (d) {
var oldxy = {x: d.x, y: d.y},
coords = xffn(oldxy, dir(d));
d.oldxy = oldxy;
d.px = d.x = coords.x;
d.py = d.y = coords.y;
});
} else {
network.nodes.forEach(function (d) {
var old = d.oldxy || {x: d.x, y: d.y};
d.px = d.x = old.x;
d.py = d.y = old.y;
delete d.oldxy;
});
}
node.transition()
.duration(obview.time)
.attr(tickStuff.nodeAttr);
link.transition()
.duration(obview.time)
.attr(tickStuff.linkAttr);
linkLabel.transition()
.duration(obview.time)
.attr(tickStuff.linkLabelAttr);
}
function showPlane(tag, box, dir) {
var g = obview.plane[tag];
// set box origin at center..
box.x = -box.width/2;
box.y = -box.height/2;
g.select('rect')
.attr(box)
.attr('opacity', 0)
.transition()
.duration(obview.time)
.attr('opacity', 1)
.attr('transform', obview.obXform(box.height, dir));
}
function hidePlane(tag) {
var g = obview.plane[tag];
g.select('rect')
.transition()
.duration(obview.time)
.attr('opacity', 0)
.attr('transform', obview.noXform());
}
function insertPlanes(ox, oy) {
function ins(tag) {
var id = obview.id(tag),
g = panZoomContainer.insert('g', '#topo-G')
.attr('id', id)
.attr('transform', translate(ox,oy));
g.append('rect')
.attr('fill', obview.fill[tag])
.attr('opacity', 0);
obview.plane[tag] = g;
}
ins('opt');
ins('pkt');
}
function removePlanes() {
function rem(tag) {
var id = obview.id(tag);
panZoomContainer.select('#'+id)
.transition()
.duration(obview.time + 50)
.remove();
delete obview.plane[tag];
}
rem('opt');
rem('pkt');
}
function padBox(box, p) {
box.x -= p;
box.y -= p;
box.width += p*2;
box.height += p*2;
}
// ==============================
// Radio Button Callbacks
var layerLookup = {
host: {
endstation: 'pkt', // default, if host event does not define type
router: 'pkt',
bgpSpeaker: 'pkt'
},
device: {
switch: 'pkt',
roadm: 'opt'
},
link: {
hostLink: 'pkt',
direct: 'pkt',
indirect: '',
tunnel: '',
optical: 'opt'
}
};
function inLayer(d, layer) {
var type = d.class === 'link' ? d.type() : d.type,
look = layerLookup[d.class],
lyr = look && look[type];
return lyr === layer;
}
function unsuppressLayer(which) {
node.each(function (d) {
var node = d.el;
if (inLayer(d, which)) {
node.classed('suppressed', false);
}
});
link.each(function (d) {
var link = d.el;
if (inLayer(d, which)) {
link.classed('suppressed', false);
}
});
}
function suppressLayers(b) {
node.classed('suppressed', b);
link.classed('suppressed', b);
// d3.selectAll('svg .port').classed('inactive', false);
// d3.selectAll('svg .portText').classed('inactive', false);
}
function showAllLayers() {
suppressLayers(false);
}
function showPacketLayer() {
node.classed('suppressed', true);
link.classed('suppressed', true);
unsuppressLayer('pkt');
}
function showOpticalLayer() {
node.classed('suppressed', true);
link.classed('suppressed', true);
unsuppressLayer('opt');
}
function restoreLayerState() {
layerBtnDispatch[layerBtnSet.selected()]();
}
// ==============================
// Private functions
function safeId(s) {
return s.replace(/[^a-z0-9]/gi, '-');
}
// set the size of the given element to that of the view (reduced if padded)
function setSize(el, view, pad) {
var padding = pad ? pad * 2 : 0;
el.attr({
width: view.width() - padding,
height: view.height() - padding
});
}
function makeNodeKey(d, what) {
var port = what + 'Port';
return d[what] + '/' + d[port];
}
function makeLinkKey(d, flipped) {
var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
return one + '-' + two;
}
function findLinkById(id) {
// check to see if this is a reverse lookup, else default to given id
var key = network.revLinkToKey[id] || id;
return key && network.lookup[key];
}
function findLink(linkData, op) {
var key = makeLinkKey(linkData),
keyrev = makeLinkKey(linkData, 1),
link = network.lookup[key],
linkRev = network.lookup[keyrev],
result = {},
ldata = link || linkRev,
rawLink;
if (op === 'add') {
if (link) {
// trying to add a link that we already know about
result.ldata = link;
result.badLogic = 'addLink: link already added';
} else if (linkRev) {
// we found the reverse of the link to be added
result.ldata = linkRev;
if (linkRev.fromTarget) {
result.badLogic = 'addLink: link already added';
}
}
} else if (op === 'update') {
if (!ldata) {
result.badLogic = 'updateLink: link not found';
} else {
rawLink = link ? ldata.fromSource : ldata.fromTarget;
result.updateWith = function (data) {
$.extend(rawLink, data);
restyleLinkElement(ldata);
}
}
} else if (op === 'remove') {
if (!ldata) {
result.badLogic = 'removeLink: link not found';
} else {
rawLink = link ? ldata.fromSource : ldata.fromTarget;
if (!rawLink) {
result.badLogic = 'removeLink: link not found';
} else {
result.removeRawLink = function () {
if (link) {
// remove fromSource
ldata.fromSource = null;
if (ldata.fromTarget) {
// promote target into source position
ldata.fromSource = ldata.fromTarget;
ldata.fromTarget = null;
ldata.key = keyrev;
delete network.lookup[key];
network.lookup[keyrev] = ldata;
delete network.revLinkToKey[keyrev];
}
} else {
// remove fromTarget
ldata.fromTarget = null;
delete network.revLinkToKey[keyrev];
}
if (ldata.fromSource) {
restyleLinkElement(ldata);
} else {
removeLinkElement(ldata);
}
}
}
}
}
return result;
}
function addLinkUpdate(ldata, link) {
// add link event, but we already have the reverse link installed
ldata.fromTarget = link;
network.revLinkToKey[link.id] = ldata.key;
restyleLinkElement(ldata);
}
var allLinkTypes = 'direct indirect optical tunnel',
defaultLinkType = 'direct';
function restyleLinkElement(ldata) {
// this fn's job is to look at raw links and decide what svg classes
// need to be applied to the line element in the DOM
var el = ldata.el,
type = ldata.type(),
lw = ldata.linkWidth(),
online = ldata.online();
el.classed('link', true);
el.classed('inactive', !online);
el.classed(allLinkTypes, false);
if (type) {
el.classed(type, true);
}
el.transition()
.duration(1000)
.attr('stroke-width', linkScale(lw))
.attr('stroke', config.topo.linkBaseColor);
}
// ==============================
// Event handlers for server-pushed events
function logicError(msg) {
// TODO, report logic error to server, via websock, so it can be logged
console.warn(msg);
}
var eventDispatch = {
addInstance: addInstance,
addDevice: addDevice,
addLink: addLink,
addHost: addHost,
updateInstance: updateInstance,
updateDevice: updateDevice,
updateLink: updateLink,
updateHost: updateHost,
removeInstance: removeInstance,
removeDevice: removeDevice,
removeLink: removeLink,
removeHost: removeHost,
showDetails: showDetails,
showSummary: showSummary,
showTraffic: showTraffic
};
function addInstance(data) {
evTrace(data);
var inst = data.payload,
id = inst.id;
if (onosInstances[id]) {
updateInstance(data);
return;
}
onosInstances[id] = inst;
onosOrder.push(inst);
updateInstances();
}
function addDevice(data) {
evTrace(data);
var device = data.payload,
id = device.id,
d;
showNoDevs(false);
if (network.lookup[id]) {
updateDevice(data);
return;
}
d = createDeviceNode(device);
network.nodes.push(d);
network.lookup[id] = d;
updateNodes();
fStart();
}
function addLink(data) {
evTrace(data);
var link = data.payload,
result = findLink(link, 'add'),
bad = result.badLogic,
d = result.ldata;
if (bad) {
logicError(bad + ': ' + link.id);
return;
}
if (d) {
// we already have a backing store link for src/dst nodes
addLinkUpdate(d, link);
return;
}
// no backing store link yet
d = createLink(link);
if (d) {
network.links.push(d);
network.lookup[d.key] = d;
updateLinks();
fStart();
}
}
function addHost(data) {
evTrace(data);
var host = data.payload,
id = host.id,
d,
lnk;
if (network.lookup[id]) {
logicError('Host already added: ' + id);
return;
}
d = createHostNode(host);
network.nodes.push(d);
network.lookup[host.id] = d;
updateNodes();
lnk = createHostLink(host);
if (lnk) {
d.linkData = lnk; // cache ref on its host
network.links.push(lnk);
network.lookup[d.ingress] = lnk;
network.lookup[d.egress] = lnk;
updateLinks();
}
fStart();
}
function updateInstance(data) {
evTrace(data);
var inst = data.payload,
id = inst.id,
d = onosInstances[id];
if (d) {
$.extend(d, inst);
updateInstances();
} else {
logicError('updateInstance lookup fail. ID = "' + id + '"');
}
}
function updateDevice(data) {
evTrace(data);
var device = data.payload,
id = device.id,
d = network.lookup[id],
wasOnline;
if (d) {
wasOnline = d.online;
$.extend(d, device);
if (positionNode(d, true)) {
sendUpdateMeta(d, true);
}
updateNodes();
if (wasOnline !== d.online) {
findAttachedLinks(d.id).forEach(restyleLinkElement);
updateOfflineVisibility(d);
}
} else {
logicError('updateDevice lookup fail. ID = "' + id + '"');
}
}
function updateLink(data) {
evTrace(data);
var link = data.payload,
result = findLink(link, 'update'),
bad = result.badLogic;
if (bad) {
logicError(bad + ': ' + link.id);
return;
}
result.updateWith(link);
}
function updateHost(data) {
evTrace(data);
var host = data.payload,
id = host.id,
d = network.lookup[id];
if (d) {
$.extend(d, host);
if (positionNode(d, true)) {
sendUpdateMeta(d, true);
}
updateNodes(d);
} else {
logicError('updateHost lookup fail. ID = "' + id + '"');
}
}
function removeInstance(data) {
evTrace(data);
var inst = data.payload,
id = inst.id,
d = onosInstances[id];
if (d) {
var idx = find(id, onosOrder);
if (idx >= 0) {
onosOrder.splice(idx, 1);
}
delete onosInstances[id];
updateInstances();
} else {
logicError('updateInstance lookup fail. ID = "' + id + '"');
}
}
function removeDevice(data) {
evTrace(data);
var device = data.payload,
id = device.id,
d = network.lookup[id];
if (d) {
removeDeviceElement(d);
} else {
logicError('removeDevice lookup fail. ID = "' + id + '"');
}
}
function removeLink(data) {
evTrace(data);
var link = data.payload,
result = findLink(link, 'remove'),
bad = result.badLogic;
if (bad) {
// may have already removed link, if attached to removed device
console.warn(bad + ': ' + link.id);
return;
}
result.removeRawLink();
}
function removeHost(data) {
evTrace(data);
var host = data.payload,
id = host.id,
d = network.lookup[id];
if (d) {
removeHostElement(d, true);
} else {
// may have already removed host, if attached to removed device
console.warn('removeHost lookup fail. ID = "' + id + '"');
}
}
// the following events are server responses to user actions
function showSummary(data) {
evTrace(data);
populateSummary(data.payload);
showSummaryPane();
}
function showDetails(data) {
evTrace(data);
haveDetails = true;
populateDetails(data.payload);
if (useDetails) {
showDetailPane();
}
}
function showTraffic(data) {
evTrace(data);
var paths = data.payload.paths,
hasTraffic = false;
// Revert any links hilighted previously.
link.style('stroke-width', null)
.classed('primary secondary animated optical', false);
// Remove all previous labels.
removeLinkLabels();
// Now hilight all links in the paths payload, and attach
// labels to them, if they are defined.
paths.forEach(function (p) {
var n = p.links.length,
i,
ldata;
hasTraffic = hasTraffic || p.traffic;
for (i=0; i<n; i++) {
ldata = findLinkById(p.links[i]);
if (ldata && ldata.el) {
ldata.el.classed(p.class, true);
ldata.label = p.labels[i];
}
}
});
updateLinks();
if (hasTraffic && !antTimer) {
startAntTimer();
} else if (!hasTraffic && antTimer) {
stopAntTimer();
}
}
// ...............................
function unknownEvent(data) {
console.warn('Unknown event type: "' + data.event + '"', data);
}
function handleServerEvent(data) {
var fn = eventDispatch[data.event] || unknownEvent;
fn(data);
}
// ==============================
// Out-going messages...
function nSel() {
return selectOrder.length;
}
function getSel(idx) {
return selections[selectOrder[idx]];
}
function allSelectionsClass(cls) {
for (var i=0, n=nSel(); i<n; i++) {
if (getSel(i).obj.class !== cls) {
return false;
}
}
return true;
}
function toggleInstances() {
if (!oiBox.isVisible()) {
showInstances();
} else {
hideInstances();
}
}
function showInstances() {
oiBox.show();
colorAffinity = true;
updateDeviceColors();
}
function hideInstances() {
oiBox.hide();
colorAffinity = false;
cancelAffinity();
updateDeviceColors();
}
function equalizeMasters() {
sendMessage('equalizeMasters');
flash('Equalizing master roles');
}
function toggleSummary() {
if (!summaryPane.isVisible()) {
requestSummary();
} else {
cancelSummary();
}
}
function requestSummary() {
sendMessage('requestSummary');
}
function cancelSummary() {
sendMessage('cancelSummary');
hideSummaryPane();
}
function toggleDetails() {
useDetails = !useDetails;
if (useDetails) {
flash('Enable details pane');
if (haveDetails) {
showDetailPane();
}
} else {
flash('Disable details pane');
hideDetailPane();
}
}
// encapsulate interaction between summary and details panes
function showSummaryPane() {
if (detailPane.isVisible()) {
detailPane.down(summaryPane.show);
} else {
summaryPane.show();
}
}
function hideSummaryPane() {
summaryPane.hide(function () {
if (detailPane.isVisible()) {
detailPane.up();
}
});
}
function showDetailPane() {
if (summaryPane.isVisible()) {
detailPane.down(detailPane.show);
} else {
detailPane.up(detailPane.show);
}
}
function hideDetailPane() {
detailPane.hide();
}
// request details for the selected element
// invoked from selection of a single node.
function requestDetails() {
var data = getSel(0).obj;
sendMessage('requestDetails', {
id: data.id,
class: data.class
});
}
function addHostIntentAction() {
sendMessage('addHostIntent', {
one: selectOrder[0],
two: selectOrder[1],
ids: selectOrder
});
flash('Host-to-Host flow added');
}
function addMultiSourceIntentAction() {
sendMessage('addMultiSourceIntent', {
src: selectOrder.slice(0, selectOrder.length - 1),
dst: selectOrder[selectOrder.length - 1],
ids: selectOrder
});
flash('Multi-Source flow added');
}
function cancelTraffic() {
sendMessage('cancelTraffic');
}
function requestTrafficForMode() {
if (hoverMode === hoverModeFlows) {
requestDeviceLinkFlows();
} else if (hoverMode === hoverModeIntents) {
requestRelatedIntents();
}
}
function showRelatedIntentsAction() {
hoverMode = hoverModeIntents;
requestRelatedIntents();
flash('Related Paths');
}
function requestRelatedIntents() {
function hoverValid() {
return hoverMode === hoverModeIntents &&
hovered &&
(hovered.class === 'host' || hovered.class === 'device');
}
if (validateSelectionContext()) {
sendMessage('requestRelatedIntents', {
ids: selectOrder,
hover: hoverValid() ? hovered.id : ''
});
}
}
function showNextIntentAction() {
hoverMode = hoverModeNone;
sendMessage('requestNextRelatedIntent');
flash('>');
}
function showPrevIntentAction() {
hoverMode = hoverModeNone;
sendMessage('requestPrevRelatedIntent');
flash('<');
}
function showSelectedIntentTrafficAction() {
hoverMode = hoverModeNone;
sendMessage('requestSelectedIntentTraffic');
flash('Traffic on Selected Path');
}
function showDeviceLinkFlowsAction() {
hoverMode = hoverModeFlows;
requestDeviceLinkFlows();
flash('Device Flows');
}
function requestDeviceLinkFlows() {
function hoverValid() {
return hoverMode === hoverModeFlows &&
hovered && (hovered.class === 'device');
}
if (validateSelectionContext()) {
sendMessage('requestDeviceLinkFlows', {
ids: selectOrder,
hover: hoverValid() ? hovered.id : ''
});
}
}
function showAllTrafficAction() {
hoverMode = hoverModeAll;
requestAllTraffic();
flash('All Traffic');
}
function requestAllTraffic() {
sendMessage('requestAllTraffic');
}
function validateSelectionContext() {
if (!hovered && nSel() === 0) {
cancelTraffic();
return false;
}
return true;
}
// ==============================
// onos instance panel functions
var instCfg = {
rectPad: 8,
nodeOx: 9,
nodeOy: 9,
nodeDim: 40,
birdOx: 19,
birdOy: 21,
birdDim: 21,
uiDy: 45,
titleDy: 30,
textYOff: 20,
textYSpc: 15
};
function viewBox(dim) {
return '0 0 ' + dim.w + ' ' + dim.h;
}
function instRectAttr(dim) {
var pad = instCfg.rectPad;
return {
x: pad,
y: pad,
width: dim.w - pad*2,
height: dim.h - pad*2,
rx: 6
};
}
function computeDim(self) {
var css = window.getComputedStyle(self);
return {
w: stripPx(css.width),
h: stripPx(css.height)
};
}
function updateInstances() {
var onoses = oiBox.el.selectAll('.onosInst')
.data(onosOrder, function (d) { return d.id; }),
instDim = {w:0,h:0},
c = instCfg;
function nSw(n) {
return '# Switches: ' + n;
}
// operate on existing onos instances if necessary
onoses.each(function (d) {
var el = d3.select(this),
svg = el.select('svg');
instDim = computeDim(this);
// update online state
el.classed('online', d.online);
// update ui-attached state
svg.select('use.uiBadge').remove();
if (d.uiAttached) {
attachUiBadge(svg);
}
function updAttr(id, value) {
svg.select('text.instLabel.'+id).text(value);
}
updAttr('ip', d.ip);
updAttr('ns', nSw(d.switches));
});
// operate on new onos instances
var entering = onoses.enter()
.append('div')
.attr('class', 'onosInst')
.classed('online', function (d) { return d.online; })
.on('click', clickInst);
entering.each(function (d) {
var el = d3.select(this),
rectAttr,
svg;
instDim = computeDim(this);
rectAttr = instRectAttr(instDim);
svg = el.append('svg').attr({
width: instDim.w,
height: instDim.h,
viewBox: viewBox(instDim)
});
svg.append('rect').attr(rectAttr);
appendBadge(svg, 14, 14, 28, '#bird');
if (d.uiAttached) {
attachUiBadge(svg);
}
var left = c.nodeOx + c.nodeDim,
len = rectAttr.width - left,
hlen = len / 2,
midline = hlen + left;
// title
svg.append('text')
.attr({
class: 'instTitle',
x: midline,
y: c.titleDy
})
.text(d.id);
// a couple of attributes
var ty = c.titleDy + c.textYOff;
function addAttr(id, label) {
svg.append('text').attr({
class: 'instLabel ' + id,
x: midline,
y: ty
}).text(label);
ty += c.textYSpc;
}
addAttr('ip', d.ip);
addAttr('ns', nSw(d.switches));
});
// operate on existing + new onoses here
// set the affinity colors...
onoses.each(function (d) {
var el = d3.select(this),
rect = el.select('svg').select('rect'),
col = instColor(d.id, d.online);
rect.style('fill', col);
});
// adjust the panel size appropriately...
oiBox.width(instDim.w * onosOrder.length);
oiBox.height(instDim.h);
// remove any outgoing instances
onoses.exit().remove();
}
function instColor(id, online) {
return cat7.get(id, !online, network.view.getTheme());
}
function clickInst(d) {
var el = d3.select(this),
aff = el.classed('affinity');
if (!aff) {
setAffinity(el, d);
} else {
cancelAffinity();
}
}
function setAffinity(el, d) {
d3.selectAll('.onosInst')
.classed('mastership', true)
.classed('affinity', false);
el.classed('affinity', true);
suppressLayers(true);
node.each(function (n) {
if (n.master === d.id) {
n.el.classed('suppressed', false);
}
});
oiShowMaster = true;
}
function cancelAffinity() {
d3.selectAll('.onosInst')
.classed('mastership affinity', false);
restoreLayerState();
oiShowMaster = false;
}
// TODO: these should be moved out to utility module.
function stripPx(s) {
return s.replace(/px$/,'');
}
function appendUse(svg, ox, oy, dim, iid, cls) {
var use = svg.append('use').attr({
transform: translate(ox,oy),
'xlink:href': iid,
width: dim,
height: dim
});
if (cls) {
use.classed(cls, true);
}
return use;
}
function appendGlyph(svg, ox, oy, dim, iid, cls) {
appendUse(svg, ox, oy, dim, iid, cls).classed('glyphIcon', true);
}
function appendBadge(svg, ox, oy, dim, iid, cls) {
appendUse(svg, ox, oy, dim, iid, cls).classed('badgeIcon', true);
}
function attachUiBadge(svg) {
appendBadge(svg, 12, instCfg.uiDy, 30, '#uiAttached', 'uiBadge');
}
function visVal(b) {
return b ? 'visible' : 'hidden';
}
// ==============================
// force layout modification functions
function translate(x, y) {
return 'translate(' + x + ',' + y + ')';
}
function scale(x,y) {
return 'scale(' + x + ',' + y + ')';
}
function skewX(x) {
return 'skewX(' + x + ')';
}
function rotate(deg) {
return 'rotate(' + deg + ')';
}
function missMsg(what, id) {
return '\n[' + what + '] "' + id + '" missing ';
}
function linkEndPoints(srcId, dstId) {
var srcNode = network.lookup[srcId],
dstNode = network.lookup[dstId],
sMiss = !srcNode ? missMsg('src', srcId) : '',
dMiss = !dstNode ? missMsg('dst', dstId) : '';
if (sMiss || dMiss) {
logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
return null;
}
return {
source: srcNode,
target: dstNode,
x1: srcNode.x,
y1: srcNode.y,
x2: dstNode.x,
y2: dstNode.y
};
}
function createHostLink(host) {
var src = host.id,
dst = host.cp.device,
id = host.ingress,
lnk = linkEndPoints(src, dst);
if (!lnk) {
return null;
}
// Synthesize link ...
$.extend(lnk, {
key: id,
class: 'link',
type: function () { return 'hostLink'; },
online: function () {
// hostlink target is edge switch
return lnk.target.online;
},
linkWidth: function () { return 1; }
});
return lnk;
}
function createLink(link) {
var lnk = linkEndPoints(link.src, link.dst);
if (!lnk) {
return null;
}
$.extend(lnk, {
key: link.id,
class: 'link',
fromSource: link,
// functions to aggregate dual link state
type: function () {
var s = lnk.fromSource,
t = lnk.fromTarget;
return (s && s.type) || (t && t.type) || defaultLinkType;
},
online: function () {
var s = lnk.fromSource,
t = lnk.fromTarget,
both = lnk.source.online && lnk.target.online;
return both && ((s && s.online) || (t && t.online));
},
linkWidth: function () {
var s = lnk.fromSource,
t = lnk.fromTarget,
ws = (s && s.linkWidth) || 0,
wt = (t && t.linkWidth) || 0;
return Math.max(ws, wt);
}
});
return lnk;
}
function removeLinkLabels() {
network.links.forEach(function (d) {
d.label = '';
});
}
function showHostVis(el) {
el.style('visibility', visVal(showHosts));
}
var widthRatio = 1.4,
linkScale = d3.scale.linear()
.domain([1, 12])
.range([widthRatio, 12 * widthRatio])
.clamp(true);
function updateLinks() {
link = linkG.selectAll('.link')
.data(network.links, function (d) { return d.key; });
// operate on existing links, if necessary
// link .foo() .bar() ...
// operate on entering links:
var entering = link.enter()
.append('line')
.attr({
x1: function (d) { return d.x1; },
y1: function (d) { return d.y1; },
x2: function (d) { return d.x2; },
y2: function (d) { return d.y2; },
stroke: config.topo.linkInColor,
'stroke-width': config.topo.linkInWidth
});
// augment links
entering.each(function (d) {
var link = d3.select(this);
// provide ref to element selection from backing data....
d.el = link;
restyleLinkElement(d);
if (d.type() === 'hostLink') {
showHostVis(link);
}
});
// operate on both existing and new links, if necessary
//link .foo() .bar() ...
// apply or remove labels
var labelData = getLabelData();
applyLinkLabels(labelData);
// operate on exiting links:
link.exit()
.attr('stroke-dasharray', '3 3')
.style('opacity', 0.5)
.transition()
.duration(1500)
.attr({
'stroke-dasharray': '3 12',
stroke: config.topo.linkOutColor,
'stroke-width': config.topo.linkOutWidth
})
.style('opacity', 0.0)
.remove();
// NOTE: invoke a single tick to force the labels to position
// onto their links.
tick();
}
function getLabelData() {
// create the backing data for showing labels..
var data = [];
link.each(function (d) {
if (d.label) {
data.push({
id: 'lab-' + d.key,
key: d.key,
label: d.label,
ldata: d
});
}
});
return data;
}
var linkLabelOffset = '0.3em';
function applyLinkLabels(data) {
var entering;
linkLabel = linkLabelG.selectAll('.linkLabel')
.data(data, function (d) { return d.id; });
// for elements already existing, we need to update the text
// and adjust the rectangle size to fit
linkLabel.each(function (d) {
var el = d3.select(this),
rect = el.select('rect'),
text = el.select('text');
text.text(d.label);
rect.attr(rectAroundText(el));
});
entering = linkLabel.enter().append('g')
.classed('linkLabel', true)
.attr('id', function (d) { return d.id; });
entering.each(function (d) {
var el = d3.select(this),
rect,
text,
parms = {
x1: d.ldata.x1,
y1: d.ldata.y1,
x2: d.ldata.x2,
y2: d.ldata.y2
};
d.el = el;
rect = el.append('rect');
text = el.append('text').text(d.label);
rect.attr(rectAroundText(el));
text.attr('dy', linkLabelOffset);
el.attr('transform', transformLabel(parms));
});
// Remove any labels that are no longer required.
linkLabel.exit().remove();
}
function rectAroundText(el) {
var text = el.select('text'),
box = text.node().getBBox();
// translate the bbox so that it is centered on [x,y]
box.x = -box.width / 2;
box.y = -box.height / 2;
// add padding
box.x -= 1;
box.width += 2;
return box;
}
function transformLabel(p) {
var dx = p.x2 - p.x1,
dy = p.y2 - p.y1,
xMid = dx/2 + p.x1,
yMid = dy/2 + p.y1;
return translate(xMid, yMid);
}
function createDeviceNode(device) {
// start with the object as is
var node = device,
type = device.type,
svgCls = type ? 'node device ' + type : 'node device';
// Augment as needed...
node.class = 'device';
node.svgClass = device.online ? svgCls + ' online' : svgCls;
positionNode(node);
return node;
}
function createHostNode(host) {
// start with the object as is
var node = host;
// Augment as needed...
node.class = 'host';
if (!node.type) {
node.type = 'endstation';
}
node.svgClass = 'node host ' + node.type;
positionNode(node);
return node;
}
function positionNode(node, forUpdate) {
var meta = node.metaUi,
x = meta && meta.x,
y = meta && meta.y,
xy;
// If we have [x,y] already, use that...
if (x && y) {
node.fixed = true;
node.px = node.x = x;
node.py = node.y = y;
return;
}
var location = node.location;
if (location && location.type === 'latlng') {
var coord = geoMapProj([location.lng, location.lat]);
node.fixed = true;
node.px = node.x = coord[0];
node.py = node.y = coord[1];
return true;
}
// if this is a node update (not a node add).. skip randomizer
if (forUpdate) {
return;
}
// Note: Placing incoming unpinned nodes at exactly the same point
// (center of the view) causes them to explode outwards when
// the force layout kicks in. So, we spread them out a bit
// initially, to provide a more serene layout convergence.
// Additionally, if the node is a host, we place it near
// the device it is connected to.
function spread(s) {
return Math.floor((Math.random() * s) - s/2);
}
function randDim(dim) {
return dim / 2 + spread(dim * 0.7071);
}
function rand() {
return {
x: randDim(network.view.width()),
y: randDim(network.view.height())
};
}
function near(node) {
var min = 12,
dx = spread(12),
dy = spread(12);
return {
x: node.x + min + dx,
y: node.y + min + dy
};
}
function getDevice(cp) {
var d = network.lookup[cp.device];
return d || rand();
}
xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
$.extend(node, xy);
}
function iconGlyphUrl(d) {
var which = d.type || 'unknown';
return '#' + which;
}
// returns the newly computed bounding box of the rectangle
function adjustRectToFitText(n) {
var text = n.select('text'),
box = text.node().getBBox(),
lab = config.labels;
text.attr('text-anchor', 'middle')
.attr('y', '-0.8em')
.attr('x', lab.imgPad/2);
// translate the bbox so that it is centered on [x,y]
box.x = -box.width / 2;
box.y = -box.height / 2;
// add padding
box.x -= (lab.padLR + lab.imgPad/2);
box.width += lab.padLR * 2 + lab.imgPad;
box.y -= lab.padTB;
box.height += lab.padTB * 2;
return box;
}
function mkSvgClass(d) {
return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
}
function hostLabel(d) {
var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
return d.labels[idx];
}
function deviceLabel(d) {
var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
return d.labels[idx];
}
function trimLabel(label) {
return (label && label.trim()) || '';
}
function emptyBox() {
return {
x: -2,
y: -2,
width: 4,
height: 4
};
}
function updateDeviceLabel(d) {
var label = trimLabel(deviceLabel(d)),
noLabel = !label,
node = d.el,
box,
dx,
dy,
cfg = config.icons.device;
node.select('text')
.text(label)
.style('opacity', 0)
.transition()
.style('opacity', 1);
if (noLabel) {
box = emptyBox();
dx = -cfg.dim/2;
dy = -cfg.dim/2;
} else {
box = adjustRectToFitText(node);
dx = box.x + cfg.xoff;
dy = box.y + cfg.yoff;
}
node.select('rect')
.transition()
.attr(box);
node.select('g.deviceIcon')
.transition()
.attr('transform', translate(dx, dy));
}
function updateHostLabel(d) {
var label = trimLabel(hostLabel(d));
d.el.select('text').text(label);
}
function updateHostVisibility() {
var v = visVal(showHosts);
nodeG.selectAll('.host').style('visibility', v);
linkG.selectAll('.hostLink').style('visibility', v);
}
function findOfflineNodes() {
var a = [];
network.nodes.forEach(function (d) {
if (d.class === 'device' && !d.online) {
a.push(d);
}
});
return a;
}
function updateOfflineVisibility(dev) {
var so = showOffline,
sh = showHosts,
vb = 'visibility',
v, off, al, ah, db, b;
function updAtt(show) {
al.forEach(function (d) {
b = show && ((d.type() !== 'hostLink') || sh);
d.el.style(vb, visVal(b));
});
ah.forEach(function (d) {
b = show && sh;
d.el.style(vb, visVal(b));
});
}
if (dev) {
// updating a specific device that just toggled off/on-line
db = dev.online || so;
dev.el.style(vb, visVal(db));
al = findAttachedLinks(dev.id);
ah = findAttachedHosts(dev.id);
updAtt(db);
} else {
// updating all offline devices
v = visVal(so);
off = findOfflineNodes();
off.forEach(function (d) {
d.el.style(vb, v);
al = findAttachedLinks(d.id);
ah = findAttachedHosts(d.id);
updAtt(so);
});
}
}
function nodeMouseOver(d) {
if (hovered != d) {
hovered = d;
requestTrafficForMode();
}
}
function nodeMouseOut(d) {
if (hovered != null) {
hovered = null;
requestTrafficForMode();
}
}
function addHostIcon(node, radius, iid) {
var dim = radius * 1.5,
xlate = -dim / 2;
node.append('use').attr({
class: 'glyphIcon hostIcon',
transform: translate(xlate,xlate),
'xlink:href': iid,
width: dim,
height: dim
});
}
function updateNodes() {
node = nodeG.selectAll('.node')
.data(network.nodes, function (d) { return d.id; });
// operate on existing nodes...
node.filter('.device').each(function (d) {
var node = d.el;
node.classed('online', d.online);
updateDeviceLabel(d);
positionNode(d, true);
});
node.filter('.host').each(function (d) {
updateHostLabel(d);
positionNode(d, true);
});
// operate on entering nodes:
var entering = node.enter()
.append('g')
.attr({
id: function (d) { return safeId(d.id); },
class: mkSvgClass,
transform: function (d) { return translate(d.x, d.y); },
opacity: 0
})
.call(network.drag)
.on('mouseover', nodeMouseOver)
.on('mouseout', nodeMouseOut)
.transition()
.attr('opacity', 1);
// augment device nodes...
entering.filter('.device').each(function (d) {
var node = d3.select(this),
label = trimLabel(deviceLabel(d)),
noLabel = !label,
box;
// provide ref to element from backing data....
d.el = node;
node.append('rect').attr({ rx: 5, ry: 5 });
node.append('text').text(label).attr('dy', '1.1em');
box = adjustRectToFitText(node);
node.select('rect').attr(box);
addDeviceIcon(node, box, noLabel, iconGlyphUrl(d));
});
// augment host nodes...
entering.filter('.host').each(function (d) {
var node = d3.select(this),
cfg = config.icons.host,
r = cfg.radius[d.type] || cfg.defaultRadius,
textDy = r + 10,
iid = iconGlyphUrl(d);
// provide ref to element from backing data....
d.el = node;
showHostVis(node);
node.append('circle').attr('r', r);
if (iid) {
addHostIcon(node, r, iid);
}
node.append('text')
.text(hostLabel)
.attr('dy', textDy)
.attr('text-anchor', 'middle');
});
// operate on both existing and new nodes, if necessary
updateDeviceColors();
// operate on exiting nodes:
// Note that the node is removed after 2 seconds.
// Sub element animations should be shorter than 2 seconds.
var exiting = node.exit()
.transition()
.duration(2000)
.style('opacity', 0)
.remove();
// host node exits....
exiting.filter('.host').each(function (d) {
var node = d.el;
node.select('use')
.style('opacity', 0.5)
.transition()
.duration(800)
.style('opacity', 0);
node.select('text')
.style('opacity', 0.5)
.transition()
.duration(800)
.style('opacity', 0);
node.select('circle')
.style('stroke-fill', '#555')
.style('fill', '#888')
.style('opacity', 0.5)
.transition()
.duration(1500)
.attr('r', 0);
});
// device node exits....
exiting.filter('.device').each(function (d) {
var node = d.el;
node.select('use')
.style('opacity', 0.5)
.transition()
.duration(800)
.style('opacity', 0);
node.selectAll('rect')
.style('stroke-fill', '#555')
.style('fill', '#888')
.style('opacity', 0.5);
});
fResume();
}
var dCol = {
black: '#000',
paleblue: '#acf',
offwhite: '#ddd',
midgrey: '#888',
lightgrey: '#bbb',
orange: '#f90'
};
// note: these are the device icon colors without affinity
var dColTheme = {
light: {
online: {
glyph: dCol.black,
rect: dCol.paleblue
},
offline: {
glyph: dCol.midgrey,
rect: dCol.lightgrey
}
},
// TODO: theme
dark: {
online: {
glyph: dCol.black,
rect: dCol.paleblue
},
offline: {
glyph: dCol.midgrey,
rect: dCol.lightgrey
}
}
};
function devBaseColor(d) {
var t = network.view.getTheme(),
o = d.online ? 'online' : 'offline';
return dColTheme[t][o];
}
function setDeviceColor(d) {
var o = d.online,
s = d.el.classed('selected'),
c = devBaseColor(d),
a = instColor(d.master, o),
g, r,
icon = d.el.select('g.deviceIcon');
if (s) {
g = c.glyph;
r = dColTheme.sel;
} else if (colorAffinity) {
g = o ? a : c.glyph;
r = o ? dCol.offwhite : a;
} else {
g = c.glyph;
r = c.rect;
}
icon.select('use')
.style('fill', g);
icon.select('rect')
.style('fill', r);
}
function addDeviceIcon(node, box, noLabel, iid) {
var cfg = config.icons.device,
dx,
dy,
g;
if (noLabel) {
dx = -cfg.dim/2;
dy = -cfg.dim/2;
} else {
box = adjustRectToFitText(node);
dx = box.x + cfg.xoff;
dy = box.y + cfg.yoff;
}
g = node.append('g')
.attr('class', 'glyphIcon deviceIcon')
.attr('transform', translate(dx, dy));
g.append('rect').attr({
x: 0,
y: 0,
rx: cfg.rx,
width: cfg.dim,
height: cfg.dim
});
g.append('use').attr({
'xlink:href': iid,
width: cfg.dim,
height: cfg.dim
});
}
function find(key, array, tag) {
var _tag = tag || 'id',
idx, n, d;
for (idx = 0, n = array.length; idx < n; idx++) {
d = array[idx];
if (d[_tag] === key) {
return idx;
}
}
return -1;
}
function removeLinkElement(d) {
var idx = find(d.key, network.links, 'key'),
removed;
if (idx >=0) {
// remove from links array
removed = network.links.splice(idx, 1);
// remove from lookup cache
delete network.lookup[removed[0].key];
updateLinks();
fResume();
}
}
function removeHostElement(d, upd) {
var lu = network.lookup;
// first, remove associated hostLink...
removeLinkElement(d.linkData);
// remove hostLink bindings
delete lu[d.ingress];
delete lu[d.egress];
// remove from lookup cache
delete lu[d.id];
// remove from nodes array
var idx = find(d.id, network.nodes);
network.nodes.splice(idx, 1);
// remove from SVG
// NOTE: upd is false if we were called from removeDeviceElement()
if (upd) {
updateNodes();
fResume();
}
}
function removeDeviceElement(d) {
var id = d.id;
// first, remove associated hosts and links..
findAttachedHosts(id).forEach(removeHostElement);
findAttachedLinks(id).forEach(removeLinkElement);
// remove from lookup cache
delete network.lookup[id];
// remove from nodes array
var idx = find(id, network.nodes);
network.nodes.splice(idx, 1);
if (!network.nodes.length) {
showNoDevs(true);
}
// remove from SVG
updateNodes();
fResume();
}
function findAttachedHosts(devId) {
var hosts = [];
network.nodes.forEach(function (d) {
if (d.class === 'host' && d.cp.device === devId) {
hosts.push(d);
}
});
return hosts;
}
function findAttachedLinks(devId) {
var links = [];
network.links.forEach(function (d) {
if (d.source.id === devId || d.target.id === devId) {
links.push(d);
}
});
return links;
}
function fResume() {
if (!oblique) {
network.force.resume();
}
}
function fStart() {
if (!oblique) {
network.force.start();
}
}
var tickStuff = {
nodeAttr: {
transform: function (d) { return translate(d.x, d.y); }
},
linkAttr: {
x1: function (d) { return d.source.x; },
y1: function (d) { return d.source.y; },
x2: function (d) { return d.target.x; },
y2: function (d) { return d.target.y; }
},
linkLabelAttr: {
transform: function (d) {
var lnk = findLinkById(d.key);
if (lnk) {
var parms = {
x1: lnk.source.x,
y1: lnk.source.y,
x2: lnk.target.x,
y2: lnk.target.y
};
return transformLabel(parms);
}
}
}
};
function tick() {
node.attr(tickStuff.nodeAttr);
link.attr(tickStuff.linkAttr);
linkLabel.attr(tickStuff.linkLabelAttr);
}
// ==============================
// Web-Socket for live data
function findGuiSuccessor() {
var idx = -1;
onosOrder.forEach(function (d, i) {
if (d.uiAttached) {
idx = i;
}
});
for (var i = 0; i < onosOrder.length - 1; i++) {
var ni = (idx + 1 + i) % onosOrder.length;
if (onosOrder[ni].online) {
return onosOrder[ni].ip;
}
}
return null;
}
function webSockUrl() {
var url = document.location.toString()
.replace(/\#.*/, '')
.replace('http://', 'ws://')
.replace('https://', 'wss://')
.replace('index.html', config.webSockUrl);
if (guiSuccessor) {
url = url.replace(location.hostname, guiSuccessor);
}
return url;
}
webSock = {
ws : null,
retries: 0,
connect : function() {
webSock.ws = new WebSocket(webSockUrl());
webSock.ws.onopen = function() {
noWebSock(false);
requestSummary();
showInstances();
webSock.retries = 0;
};
webSock.ws.onmessage = function(m) {
if (m.data) {
wsTraceRx(m.data);
handleServerEvent(JSON.parse(m.data));
}
};
webSock.ws.onclose = function(m) {
webSock.ws = null;
guiSuccessor = findGuiSuccessor();
if (guiSuccessor && webSock.retries < onosOrder.length) {
webSock.retries++;
webSock.connect();
} else {
noWebSock(true);
}
};
},
send : function(text) {
if (text != null) {
webSock._send(text);
}
},
_send : function(message) {
if (webSock.ws) {
webSock.ws.send(message);
} else if (config.useLiveData) {
console.warn('no web socket open', message);
} else {
console.log('WS Send: ', message);
}
}
};
function noWebSock(b) {
mask.style('display',b ? 'block' : 'none');
}
function sendMessage(evType, payload) {
var p = payload || {},
toSend = {
event: evType,
sid: ++sid,
payload: p
},
asText = JSON.stringify(toSend);
wsTraceTx(asText);
webSock.send(asText);
// Temporary measure for debugging UI behavior ...
if (!config.useLiveData) {
handleTestSend(toSend);
}
}
function wsTraceTx(msg) {
wsTrace('tx', msg);
}
function wsTraceRx(msg) {
wsTrace('rx', msg);
}
function wsTrace(rxtx, msg) {
// console.log('[' + rxtx + '] ' + msg);
}
function handleTestSend(msg) { }
// ==============================
// Selection stuff
function selectObject(obj, el) {
var n,
ev = d3.event.sourceEvent;
// if the meta or alt key is pressed, we are panning/zooming, so ignore
if (ev.metaKey || ev.altKey) {
return;
}
if (el) {
n = d3.select(el);
} else {
node.each(function(d) {
if (d == obj) {
n = d3.select(el = this);
}
});
}
if (!n) return;
if (ev.shiftKey && n.classed('selected')) {
deselectObject(obj.id);
updateDetailPane();
return;
}
if (!ev.shiftKey) {
deselectAll();
}
selections[obj.id] = { obj: obj, el: el };
selectOrder.push(obj.id);
n.classed('selected', true);
updateDeviceColors(obj);
updateDetailPane();
}
function deselectObject(id) {
var obj = selections[id],
idx;
if (obj) {
d3.select(obj.el).classed('selected', false);
delete selections[id];
idx = $.inArray(id, selectOrder);
if (idx >= 0) {
selectOrder.splice(idx, 1);
}
updateDeviceColors(obj.obj);
}
}
function deselectAll() {
// deselect all nodes in the network...
node.classed('selected', false);
selections = {};
selectOrder = [];
updateDeviceColors();
updateDetailPane();
}
function updateDeviceColors(d) {
if (d) {
setDeviceColor(d);
} else {
node.filter('.device').each(function (d) {
setDeviceColor(d);
});
}
}
// update the state of the detail pane, based on current selections
function updateDetailPane() {
var nSel = selectOrder.length;
if (!nSel) {
emptySelect();
} else if (nSel === 1) {
singleSelect();
} else {
multiSelect();
}
}
function emptySelect() {
haveDetails = false;
hideDetailPane();
cancelTraffic();
}
function singleSelect() {
// NOTE: detail is shown from showDetails event callback
requestDetails();
cancelTraffic();
requestTrafficForMode();
}
function multiSelect() {
haveDetails = true;
populateMultiSelect();
cancelTraffic();
requestTrafficForMode();
}
function addSep(tbody) {
var tr = tbody.append('tr');
$('<hr>').appendTo(tr.append('td').attr('colspan', 2));
}
function addProp(tbody, label, value) {
var tr = tbody.append('tr');
tr.append('td')
.attr('class', 'label')
.text(label + ' :');
tr.append('td')
.attr('class', 'value')
.text(value);
}
function populateMultiSelect() {
detailPane.empty();
var title = detailPane.append('h3'),
table = detailPane.append('table'),
tbody = table.append('tbody');
title.text('Selected Nodes');
selectOrder.forEach(function (d, i) {
addProp(tbody, i+1, d);
});
addMultiSelectActions();
}
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();
var svg = detailPane.append('svg'),
iid = iconGlyphUrl(data);
var title = detailPane.append('h2'),
table = detailPane.append('table'),
tbody = table.append('tbody');
appendGlyph(svg, 0, 0, 40, iid);
title.text(data.id);
data.propOrder.forEach(function(p) {
if (p === '-') {
addSep(tbody);
} else {
addProp(tbody, p, data.props[p]);
}
});
addSingleSelectActions(data);
}
function addSingleSelectActions(data) {
detailPane.append('hr');
// always want to allow 'show traffic'
addAction(detailPane, 'Show Related Traffic', showRelatedIntentsAction);
if (data.type === 'switch') {
addAction(detailPane, 'Show Device Flows', showDeviceLinkFlowsAction);
}
}
function addMultiSelectActions() {
detailPane.append('hr');
// always want to allow 'show traffic'
addAction(detailPane, 'Show Related Traffic', showRelatedIntentsAction);
// if exactly two hosts are selected, also want 'add host intent'
if (nSel() === 2 && allSelectionsClass('host')) {
addAction(detailPane, 'Create Host-to-Host Flow', addHostIntentAction);
} else if (nSel() >= 2 && allSelectionsClass('host')) {
addAction(detailPane, 'Create Multi-Source Flow', addMultiSourceIntentAction);
}
}
function addAction(panel, text, cb) {
panel.append('div')
.classed('actionBtn', true)
.text(text)
.on('click', cb);
}
// === Pan and Zoom behaviors...
function panZoom(translate, scale) {
panZoomContainer.attr('transform',
'translate(' + translate + ')scale(' + scale + ')');
// keep the map lines constant width while zooming
bgImg.style('stroke-width', 2.0 / scale + 'px');
}
function resetPanZoom() {
panZoom([0,0], 1);
zoom.translate([0,0]).scale(1);
}
function setupPanZoom() {
function zoomed() {
var ev = d3.event.sourceEvent;
// pan/zoom active when meta or alt key is pressed...
if (ev.metaKey || ev.altKey) {
panZoom(d3.event.translate, d3.event.scale);
}
}
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.scaleExtent([0.25, 10])
.on("zoom", zoomed);
svg.call(zoom);
}
function setupNoDevices() {
var g = noDevices.append('g');
appendBadge(g, 0, 0, 100, '#bird', 'noDevsBird');
var text = g.append('text')
.text('No devices are connected')
.attr({ x: 120, y: 80});
}
function repositionNoDevices() {
var g = noDevices.select('g');
var box = g.node().getBBox();
box.x -= box.width/2;
box.y -= box.height/2;
g.attr('transform', translate(box.x, box.y));
}
// ==============================
// Test harness code
function prepareScenario(view, ctx, dbg) {
var sc = scenario,
urlSc = sc.evDir + ctx + sc.evScenario;
if (!ctx) {
view.alert("No scenario specified (null ctx)");
return;
}
sc.view = view;
sc.ctx = ctx;
sc.debug = dbg;
sc.evNumber = 0;
d3.json(urlSc, function(err, data) {
var p = data && data.params || {},
desc = data && data.description || null,
intro = data && data.title;
if (err) {
view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
} else {
sc.params = p;
if (desc) {
intro += '\n\n ' + desc.join('\n ');
}
view.alert(intro);
}
});
}
function setupDefs(svg) {
var defs = svg.append('defs');
gly.loadDefs(defs);
d3u.loadGlow(defs);
}
function sendUpdateMeta(d, store) {
var metaUi = {},
ll;
if (store) {
ll = geoMapProj.invert([d.x, d.y]);
metaUi = {
x: d.x,
y: d.y,
lng: ll[0],
lat: ll[1]
};
}
d.metaUi = metaUi;
sendMessage('updateMeta', {
id: d.id,
'class': d.class,
memento: metaUi
});
}
// ==============================
// View life-cycle callbacks
function init(view, ctx, flags) {
var w = view.width(),
h = view.height(),
logSize = config.logicalSize,
fcfg = config.force;
// NOTE: view.$div is a D3 selection of the view's div
var viewBox = '0 0 ' + logSize + ' ' + logSize;
svg = view.$div.append('svg').attr('viewBox', viewBox);
setSize(svg, view);
// load glyphs, filters, and other definitions...
setupDefs(svg);
panZoomContainer = svg.append('g').attr('id', 'panZoomContainer');
setupPanZoom();
noDevices = svg.append('g')
.attr('class', 'noDevsLayer')
.attr('transform', translate(logSize/2, logSize/2));
setupNoDevices();
// group for the topology
topoG = panZoomContainer.append('g')
.attr('id', 'topo-G');
// subgroups for links, link labels, and nodes
linkG = topoG.append('g').attr('id', 'links');
linkLabelG = topoG.append('g').attr('id', 'linkLabels');
nodeG = topoG.append('g').attr('id', 'nodes');
// selection of links, linkLabels, and nodes
link = linkG.selectAll('.link');
linkLabel = linkLabelG.selectAll('.linkLabel');
node = nodeG.selectAll('.node');
function chrg(d) {
return fcfg.charge[d.class] || -12000;
}
function ldist(d) {
return fcfg.linkDistance[d.type] || 50;
}
function lstrg(d) {
// 0.0 - 1.0
return fcfg.linkStrength[d.type] || 1.0;
}
function selectCb(d, self) {
selectObject(d, self);
}
function atDragEnd(d, self) {
// once we've finished moving, pin the node in position
d.fixed = true;
d3.select(self).classed('fixed', true);
if (config.useLiveData) {
sendUpdateMeta(d, true);
} else {
console.log('Moving node ' + d.id + ' to [' + d.x + ',' + d.y + ']');
}
}
// predicate that indicates when dragging is active
function dragEnabled() {
var ev = d3.event.sourceEvent;
// nodeLock means we aren't allowing nodes to be dragged...
// meta or alt key pressed means we are zooming/panning...
return !nodeLock && !(ev.metaKey || ev.altKey);
}
// predicate that indicates when clicking is active
function clickEnabled() {
return true;
}
// set up the force layout
network.force = d3.layout.force()
.size([w, h])
.nodes(network.nodes)
.links(network.links)
.gravity(0.4)
.friction(0.7)
.charge(chrg)
.linkDistance(ldist)
.linkStrength(lstrg)
.on('tick', tick);
network.drag = d3u.createDragBehavior(network.force,
selectCb, atDragEnd, dragEnabled, clickEnabled);
// create mask layer for when we lose connection to server.
// TODO: this should be part of the framework
function para(sel, text) {
sel.append('p').text(text);
}
mask = view.$div.append('div').attr('id','topo-mask');
para(mask, 'Oops!');
para(mask, 'Web-socket connection to server closed...');
para(mask, 'Try refreshing the page.');
mask.append('svg')
.attr({
id: 'mask-bird',
width: w,
height: h
})
.append('g')
.attr('transform', birdTranslate(w, h))
.style('opacity', 0.3)
.append('use')
.attr({
'xlink:href': '#bird',
width: config.birdDim,
height: config.birdDim,
fill: '#111'
})
}
function load(view, ctx, flags) {
// resize, in case the window was resized while we were not loaded
resize(view, ctx, flags);
// cache the view token, so network topo functions can access it
network.view = view;
config.useLiveData = !flags.local;
if (!config.useLiveData) {
prepareScenario(view, ctx, flags.debug);
}
// set our radio buttons and key bindings
layerBtnSet = view.setRadio(layerButtons);
view.setKeys(keyDispatch);
view.setGestures(gestures);
// Load map data asynchronously; complete startup after that..
loadGeoJsonData();
}
function startAntTimer() {
// Note: disabled until traffic can be allotted to intents properly
if (false && !antTimer) {
var pulses = [5, 3, 1.2, 3],
pulse = 0;
antTimer = setInterval(function () {
pulse = pulse + 1;
pulse = pulse === pulses.length ? 0 : pulse;
d3.selectAll('.animated').style('stroke-width', pulses[pulse]);
}, 200);
}
}
function stopAntTimer() {
if (antTimer) {
clearInterval(antTimer);
antTimer = null;
}
}
function unload(view, ctx, flags) {
stopAntTimer();
}
var geoJsonUrl = 'continental_us.json',
geoJson;
function loadGeoJsonData() {
d3.json(geoJsonUrl, function (err, data) {
if (err) {
console.error('failed to load Map data', err);
} else {
geoJson = data;
loadGeoMap();
}
repositionNoDevices();
showNoDevs(true);
// finally, connect to the server...
if (config.useLiveData) {
webSock.connect();
}
});
}
function setProjForView(path, topoData) {
var dim = config.logicalSize;
// start with unit scale, no translation..
geoMapProj.scale(1).translate([0, 0]);
// figure out dimensions of map data..
var b = path.bounds(topoData),
x1 = b[0][0],
y1 = b[0][1],
x2 = b[1][0],
y2 = b[1][1],
dx = x2 - x1,
dy = y2 - y1,
x = (x1 + x2) / 2,
y = (y1 + y2) / 2;
// size map to 95% of minimum dimension to fill space..
var s = .95 / Math.min(dx / dim, dy / dim);
var t = [dim / 2 - s * x, dim / 2 - s * y];
// set new scale, translation on the projection..
geoMapProj.scale(s).translate(t);
}
function loadGeoMap() {
fnTrace('loadGeoMap', geoJsonUrl);
// extracts the topojson data into geocoordinate-based geometry
var topoData = topojson.feature(geoJson, geoJson.objects.states);
// see: http://bl.ocks.org/mbostock/4707858
geoMapProj = d3.geo.mercator();
var path = d3.geo.path().projection(geoMapProj);
setProjForView(path, topoData);
bgImg = panZoomContainer.insert("g", '#topo-G');
bgImg.attr('id', 'map').selectAll('path')
.data(topoData.features)
.enter()
.append('path')
.attr('d', path);
}
function resize(view, ctx, flags) {
var w = view.width(),
h = view.height();
setSize(svg, view);
d3.select('#mask-bird').attr({ width: w, height: h})
.select('g').attr('transform', birdTranslate(w, h));
}
function theme(view, ctx, flags) {
updateInstances();
updateDeviceColors();
}
function birdTranslate(w, h) {
var bdim = config.birdDim;
return 'translate('+((w-bdim)*.4)+','+((h-bdim)*.1)+')';
}
function isF(f) { return $.isFunction(f) ? f : null; }
function noop() {}
function augmentDetailPane() {
var dp = detailPane;
dp.ypos = { up: 64, down: 320, current: 320};
dp._move = function (y, cb) {
var endCb = isF(cb) || noop,
yp = dp.ypos;
if (yp.current !== y) {
yp.current = y;
dp.el.transition().duration(300)
.each('end', endCb)
.style('top', yp.current + 'px');
} else {
endCb();
}
};
dp.down = function (cb) { dp._move(dp.ypos.down, cb); };
dp.up = function (cb) { dp._move(dp.ypos.up, cb); };
}
// ==============================
// View registration
onos.ui.addView('topo', {
init: init,
load: load,
unload: unload,
resize: resize,
theme: theme
});
summaryPane = onos.ui.addFloatingPanel('topo-summary');
detailPane = onos.ui.addFloatingPanel('topo-detail');
augmentDetailPane();
oiBox = onos.ui.addFloatingPanel('topo-oibox', 'TL');
oiBox.width(20);
}(ONOS));
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui.impl;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.onlab.osgi.ServiceDirectory;
import org.onlab.osgi.TestServiceDirectory;
import org.onlab.packet.ChassisId;
import org.onlab.packet.IpAddress;
import org.onlab.packet.MacAddress;
import org.onlab.packet.VlanId;
import org.onosproject.cluster.ClusterService;
import org.onosproject.cluster.ClusterServiceAdapter;
import org.onosproject.core.CoreService;
import org.onosproject.core.CoreServiceAdapter;
import org.onosproject.core.Version;
import org.onosproject.mastership.MastershipService;
import org.onosproject.mastership.MastershipServiceAdapter;
import org.onosproject.net.DefaultDevice;
import org.onosproject.net.DefaultHost;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
import org.onosproject.net.HostLocation;
import org.onosproject.net.Port;
import org.onosproject.net.PortNumber;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.device.DeviceServiceAdapter;
import org.onosproject.net.flow.FlowEntry;
import org.onosproject.net.flow.FlowRuleService;
import org.onosproject.net.flow.FlowRuleServiceAdapter;
import org.onosproject.net.host.HostService;
import org.onosproject.net.host.HostServiceAdapter;
import org.onosproject.net.intent.IntentService;
import org.onosproject.net.intent.IntentServiceAdapter;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.link.LinkServiceAdapter;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.net.statistic.StatisticService;
import org.onosproject.net.statistic.StatisticServiceAdapter;
import org.onosproject.net.topology.TopologyService;
import org.onosproject.net.topology.TopologyServiceAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
public class TopologyViewWebSocketTest {
private static final ProviderId PID = new ProviderId("of", "foo.bar");
private static final ChassisId CHID = new ChassisId(123L);
private static final MacAddress MAC = MacAddress.valueOf("00:00:00:00:00:19");
private static final DeviceId DID = DeviceId.deviceId("of:foo");
private static final Set<IpAddress> IPS = ImmutableSet.of(IpAddress.valueOf("1.2.3.4"));
private TestWebSocket ws;
private TestServiceDirectory sd;
@Before
public void setUp() {
sd = new TestServiceDirectory();
sd.add(DeviceService.class, new TestDeviceService());
sd.add(ClusterService.class, new ClusterServiceAdapter());
sd.add(LinkService.class, new LinkServiceAdapter());
sd.add(HostService.class, new TestHostService());
sd.add(MastershipService.class, new MastershipServiceAdapter());
sd.add(IntentService.class, new IntentServiceAdapter());
sd.add(FlowRuleService.class, new TestFlowService());
sd.add(StatisticService.class, new StatisticServiceAdapter());
sd.add(TopologyService.class, new TopologyServiceAdapter());
sd.add(CoreService.class, new TestCoreService());
ws = new TestWebSocket(sd);
}
@Test
public void requestDetailsDevice() {
// build the request
String request = "{\"event\":\"requestDetails\", \"sid\":0, "
+ "\"payload\":{\"id\":\"of:000001\",\"class\":\"device\"}}";
ws.onMessage(request);
// look at the ws reply, and verify that it is correct
assertEquals("incorrect id", "of:000001", ws.reply.path("payload").path("id").asText());
assertEquals("incorrect mfr", "foo", ws.reply.path("payload").path("props").path("Vendor").asText());
}
@Test
public void requestDetailsHost() {
// build the request
String request = "{\"event\":\"requestDetails\", \"sid\":0, "
+ "\"payload\":{\"id\":\"00:00:00:00:00:19/-1\",\"class\":\"host\"}}";
ws.onMessage(request);
// look at the ws reply, and verify that it is correct
assertEquals("incorrect id", "00:00:00:00:00:19/-1", ws.reply.path("payload").path("id").asText());
assertEquals("incorrect ip address", "1.2.3.4", ws.reply.path("payload").path("props").path("IP").asText());
}
private class TestWebSocket extends TopologyViewWebSocket {
private ObjectNode reply;
/**
* Creates a new web-socket for serving data to GUI topology view.
*
* @param directory service directory
*/
public TestWebSocket(ServiceDirectory directory) {
super(directory);
}
@Override
protected synchronized void sendMessage(ObjectNode data) {
reply = data;
}
}
private class TestCoreService extends CoreServiceAdapter {
@Override
public Version version() {
return Version.version("1.2.3");
}
}
private class TestDeviceService extends DeviceServiceAdapter {
@Override
public Device getDevice(DeviceId deviceId) {
return new DefaultDevice(PID, deviceId, Device.Type.SWITCH,
"foo", "hw", "sw", "sn", CHID);
}
@Override
public List<Port> getPorts(DeviceId deviceId) {
return new ArrayList<>();
}
}
private class TestFlowService extends FlowRuleServiceAdapter {
@Override
public Iterable<FlowEntry> getFlowEntries(DeviceId deviceId) {
return new ArrayList<>();
}
}
private class TestHostService extends HostServiceAdapter {
@Override
public Host getHost(HostId hostId) {
return new DefaultHost(PID, hostId, MAC, VlanId.NONE,
new HostLocation(DID, PortNumber.P0, 123L), IPS);
}
}
}
\ No newline at end of file