Prince Pereira

Fix for ONOS-291. Highlighting intents in ONOS GUI for selected links.

Change-Id: I757aa40b96d92014fa2d720539da20dd309ec9b1
......@@ -19,11 +19,14 @@ package org.onosproject.ui.topo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.Device;
import org.onosproject.net.Element;
import org.onosproject.net.Host;
import org.onosproject.net.Link;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.host.HostService;
import org.onosproject.net.link.LinkService;
import org.onosproject.ui.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -33,11 +36,12 @@ import java.util.HashSet;
import java.util.Set;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.onosproject.net.ConnectPoint.deviceConnectPoint;
import static org.onosproject.net.DeviceId.deviceId;
import static org.onosproject.net.HostId.hostId;
/**
* Encapsulates a selection of devices and/or hosts from the topology view.
* Encapsulates a selection of devices, hosts and links from the topology view.
*/
public class NodeSelection {
......@@ -46,31 +50,38 @@ public class NodeSelection {
private static final String IDS = "ids";
private static final String HOVER = "hover";
private static final String LINK_ID_DELIM = "-";
private final DeviceService deviceService;
private final HostService hostService;
private final LinkService linkService;
private final Set<String> ids;
private final String hover;
private final Set<Device> devices = new HashSet<>();
private final Set<Host> hosts = new HashSet<>();
private final Set<Link> links = new HashSet<>();
private Element hovered;
/**
* Creates a node selection entity, from the given payload, using the
* supplied device and host services. Note that if a device or host was
* hovered over by the mouse, it is available via {@link #hovered()}.
* supplied link, device and host services. Note that if a link, device
* or host was hovered over by the mouse, it is available
* via {@link #hovered()}.
*
* @param payload message payload
* @param deviceService device service
* @param hostService host service
* @param linkService link service
*/
public NodeSelection(ObjectNode payload,
DeviceService deviceService,
HostService hostService) {
HostService hostService,
LinkService linkService) {
this.deviceService = deviceService;
this.hostService = hostService;
this.linkService = linkService;
ids = extractIds(payload);
hover = extractHover(payload);
......@@ -82,8 +93,9 @@ public class NodeSelection {
setHoveredElement();
}
// now go find the devices and hosts that are in the selection list
Set<String> unmatched = findDevices(ids);
// now go find the links, devices and hosts that are in the selection list
Set<String> unmatched = findLinks(ids);
unmatched = findDevices(unmatched);
unmatched = findHosts(unmatched);
if (unmatched.size() > 0) {
log.debug("Skipping unmatched IDs {}", unmatched);
......@@ -101,6 +113,15 @@ public class NodeSelection {
}
/**
* Returns a view of the selected links (hover not included).
*
* @return selected links
*/
public Set<Link> links() {
return Collections.unmodifiableSet(links);
}
/**
* Returns a view of the selected devices, including the hovered device
* if there was one.
*
......@@ -144,7 +165,24 @@ public class NodeSelection {
}
/**
* Returns the element (host or device) over which the mouse was hovering,
* Returns a view of the selected links, including the hovered link
* if thee was one.
*
* @return selected (plus hovered) links
*/
public Set<Link> linksWithHover() {
Set<Link> withHover;
if (hovered != null && hovered instanceof Link) {
withHover = new HashSet<>(links);
withHover.add((Link) hovered);
} else {
withHover = links;
}
return Collections.unmodifiableSet(withHover);
}
/**
* Returns the element (link, host or device) over which the mouse was hovering,
* or null.
*
* @return element hovered over
......@@ -159,7 +197,7 @@ public class NodeSelection {
* @return true if nothing selected
*/
public boolean none() {
return devices().size() == 0 && hosts().size() == 0;
return devices().isEmpty() && hosts().isEmpty() && links().isEmpty();
}
@Override
......@@ -169,6 +207,7 @@ public class NodeSelection {
", hover='" + hover + '\'' +
", #devices=" + devices.size() +
", #hosts=" + hosts.size() +
", #links=" + links.size() +
'}';
}
......@@ -248,4 +287,34 @@ public class NodeSelection {
}
return unmatched;
}
private Set<String> findLinks(Set<String> ids) {
Set<String> unmatched = new HashSet<>();
ConnectPoint cpSrc, cpDst;
Link link;
for (String id : ids) {
try {
String[] connectPoints = id.split(LINK_ID_DELIM);
if (connectPoints.length != 2) {
unmatched.add(id);
continue;
}
cpSrc = deviceConnectPoint(connectPoints[0]);
cpDst = deviceConnectPoint(connectPoints[1]);
link = linkService.getLink(cpSrc, cpDst);
if (link != null) {
links.add(link);
} else {
unmatched.add(id);
}
} catch (Exception e) {
unmatched.add(id);
}
}
return unmatched;
}
}
......
......@@ -21,18 +21,24 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableSet;
import org.junit.Test;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultDevice;
import org.onosproject.net.DefaultHost;
import org.onosproject.net.DefaultLink;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
import org.onosproject.net.Link;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.device.DeviceServiceAdapter;
import org.onosproject.net.host.HostService;
import org.onosproject.net.host.HostServiceAdapter;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.link.LinkServiceAdapter;
import static org.junit.Assert.*;
import static org.onosproject.net.Link.Type.DIRECT;
/**
* Unit tests for {@link NodeSelection}.
......@@ -51,20 +57,31 @@ public class NodeSelectionTest {
}
}
private static class FakeLink extends DefaultLink {
FakeLink(ConnectPoint src, ConnectPoint dst) {
super(null, src, dst, DIRECT, Link.State.ACTIVE);
}
}
private final ObjectMapper mapper = new ObjectMapper();
private static final String IDS = "ids";
private static final String HOVER = "hover";
private static final DeviceId DEVICE_1_ID = DeviceId.deviceId("Device-1");
private static final DeviceId DEVICE_2_ID = DeviceId.deviceId("Device-2");
private static final DeviceId DEVICE_1_ID = DeviceId.deviceId("Device1");
private static final DeviceId DEVICE_2_ID = DeviceId.deviceId("Device2");
private static final HostId HOST_A_ID = HostId.hostId("aa:aa:aa:aa:aa:aa/1");
private static final HostId HOST_B_ID = HostId.hostId("bb:bb:bb:bb:bb:bb/2");
private static final String LINK_1_ID = "Device1/1-Device2/2";
private static final ConnectPoint CP_SRC = ConnectPoint.deviceConnectPoint("Device1/1");
private static final ConnectPoint CP_DST = ConnectPoint.deviceConnectPoint("Device2/2");
private static final Device DEVICE_1 = new FakeDevice(DEVICE_1_ID);
private static final Device DEVICE_2 = new FakeDevice(DEVICE_2_ID);
private static final Host HOST_A = new FakeHost(HOST_A_ID);
private static final Host HOST_B = new FakeHost(HOST_B_ID);
private static final Link LINK_A = new FakeLink(CP_SRC, CP_DST);
private static final Link LINK_B = new FakeLink(CP_DST, CP_SRC);
// ==================
// == FAKE SERVICES
......@@ -94,8 +111,21 @@ public class NodeSelectionTest {
}
}
private static class FakeLinks extends LinkServiceAdapter {
@Override
public Link getLink(ConnectPoint src, ConnectPoint dst) {
if (CP_SRC.equals(src) && CP_DST.equals(dst)) {
return LINK_A;
} else if (CP_SRC.equals(dst) && CP_DST.equals(src)) {
return LINK_B;
}
return null;
}
}
private DeviceService deviceService = new FakeDevices();
private HostService hostService = new FakeHosts();
private LinkService linkService = new FakeLinks();
private NodeSelection ns;
......@@ -108,7 +138,7 @@ public class NodeSelectionTest {
}
private NodeSelection createNodeSelection(ObjectNode payload) {
return new NodeSelection(payload, deviceService, hostService);
return new NodeSelection(payload, deviceService, hostService, linkService);
}
// selection JSON payload creation methods
......@@ -134,6 +164,13 @@ public class NodeSelectionTest {
ids.add(HOST_A_ID.toString());
return payload;
}
private ObjectNode oneLinkSelected() {
ObjectNode payload = objectNode();
ArrayNode ids = arrayNode();
payload.set(IDS, ids);
ids.add(LINK_1_ID.toString());
return payload;
}
private ObjectNode twoHostsOneDeviceSelected() {
ObjectNode payload = objectNode();
......@@ -204,6 +241,21 @@ public class NodeSelectionTest {
}
@Test
public void oneLink() {
ns = createNodeSelection(oneLinkSelected());
assertEquals("unexpected devices", 0, ns.devices().size());
assertEquals("unexpected devices w/hover", 0, ns.devicesWithHover().size());
assertEquals("unexpected hosts", 0, ns.hosts().size());
assertEquals("unexpected hosts w/hover", 0, ns.hostsWithHover().size());
assertEquals("missing link", 1, ns.links().size());
assertTrue("missing link A", ns.links().contains(LINK_A));
assertEquals("missing link w/hover", 1, ns.linksWithHover().size());
assertTrue("missing link A w/hover", ns.linksWithHover().contains(LINK_A));
assertFalse("unexpected selection", ns.none());
assertNull("hover?", ns.hovered());
}
@Test
public void twoHostsOneDevice() {
ns = createNodeSelection(twoHostsOneDeviceSelected());
assertEquals("missing device", 1, ns.devices().size());
......
......@@ -534,7 +534,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void process(long sid, ObjectNode payload) {
NodeSelection nodeSelection =
new NodeSelection(payload, deviceService, hostService);
new NodeSelection(payload, deviceService, hostService, linkService);
traffic.monitor(Mode.DEV_LINK_FLOWS, nodeSelection);
}
}
......@@ -547,7 +547,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
@Override
public void process(long sid, ObjectNode payload) {
NodeSelection nodeSelection =
new NodeSelection(payload, deviceService, hostService);
new NodeSelection(payload, deviceService, hostService, linkService);
traffic.monitor(Mode.RELATED_INTENTS, nodeSelection);
}
}
......
......@@ -49,7 +49,10 @@ public class IntentSelection {
*/
public IntentSelection(NodeSelection nodes, TopoIntentFilter filter) {
this.nodes = nodes;
intents = filter.findPathIntents(nodes.hostsWithHover(), nodes.devicesWithHover());
intents = filter.findPathIntents(
nodes.hostsWithHover(),
nodes.devicesWithHover(),
nodes.linksWithHover());
if (intents.size() == 1) {
index = 0; // pre-select a single intent
}
......
......@@ -74,9 +74,12 @@ public class TopoIntentFilter {
*
* @param hosts set of hosts to query by
* @param devices set of devices to query by
* @return set of intents that 'match' all hosts and devices given
* @param links set of links to query by
* @return set of intents that 'match' all hosts, devices and links given
*/
public List<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices) {
public List<Intent> findPathIntents(Set<Host> hosts,
Set<Device> devices,
Set<Link> links) {
// start with all intents
Iterable<Intent> sourceIntents = intentService.getIntents();
......@@ -85,7 +88,7 @@ public class TopoIntentFilter {
// Iterate over all intents and produce a set that contains only those
// intents that target all selected hosts or derived edge connect points.
return getIntents(hosts, devices, edgePoints, sourceIntents);
return getIntents(hosts, devices, links, edgePoints, sourceIntents);
}
......@@ -98,12 +101,12 @@ public class TopoIntentFilter {
return edgePoints;
}
// Produces a list of intents that target all selected hosts, devices or connect points.
private List<Intent> getIntents(Set<Host> hosts, Set<Device> devices,
// Produces a list of intents that target all selected hosts, devices, links or connect points.
private List<Intent> getIntents(Set<Host> hosts, Set<Device> devices, Set<Link> links,
Set<ConnectPoint> edgePoints,
Iterable<Intent> sourceIntents) {
List<Intent> intents = new ArrayList<>();
if (hosts.isEmpty() && devices.isEmpty()) {
if (hosts.isEmpty() && devices.isEmpty() && links.isEmpty()) {
return intents;
}
......@@ -115,13 +118,13 @@ public class TopoIntentFilter {
boolean isRelevant = false;
if (intent instanceof HostToHostIntent) {
isRelevant = isIntentRelevantToHosts((HostToHostIntent) intent, hosts) &&
isIntentRelevantToDevices(intent, devices);
isIntentRelevantToDevices(intent, devices) && isIntentRelevantToLinks(intent, links);
} else if (intent instanceof PointToPointIntent) {
isRelevant = isIntentRelevant((PointToPointIntent) intent, edgePoints) &&
isIntentRelevantToDevices(intent, devices);
isIntentRelevantToDevices(intent, devices) && isIntentRelevantToLinks(intent, links);
} else if (intent instanceof MultiPointToSinglePointIntent) {
isRelevant = isIntentRelevant((MultiPointToSinglePointIntent) intent, edgePoints) &&
isIntentRelevantToDevices(intent, devices);
isIntentRelevantToDevices(intent, devices) && isIntentRelevantToLinks(intent, links);
} else if (intent instanceof OpticalConnectivityIntent) {
opticalIntents.add((OpticalConnectivityIntent) intent);
}
......@@ -167,6 +170,17 @@ public class TopoIntentFilter {
return true;
}
// Indicates whether the specified intent involves all of the given links.
private boolean isIntentRelevantToLinks(Intent intent, Iterable<Link> links) {
List<Intent> installables = intentService.getInstallableIntents(intent.key());
for (Link link : links) {
if (!isIntentRelevantToLink(installables, link)) {
return false;
}
}
return true;
}
// Indicates whether the specified intent involves the given device.
private boolean isIntentRelevantToDevice(List<Intent> installables, Device device) {
if (installables != null) {
......@@ -196,6 +210,38 @@ public class TopoIntentFilter {
return false;
}
// Indicates whether the specified intent involves the given link.
private boolean isIntentRelevantToLink(List<Intent> installables, Link link) {
Link reverseLink = linkService.getLink(link.dst(), link.src());
if (installables != null) {
for (Intent installable : installables) {
if (installable instanceof PathIntent) {
PathIntent pathIntent = (PathIntent) installable;
return pathIntent.path().links().contains(link) ||
pathIntent.path().links().contains(reverseLink);
} else if (installable instanceof FlowRuleIntent) {
FlowRuleIntent flowRuleIntent = (FlowRuleIntent) installable;
return flowRuleIntent.resources().contains(link) ||
flowRuleIntent.resources().contains(reverseLink);
} else if (installable instanceof FlowObjectiveIntent) {
FlowObjectiveIntent objectiveIntent = (FlowObjectiveIntent) installable;
return objectiveIntent.resources().contains(link) ||
objectiveIntent.resources().contains(reverseLink);
} else if (installable instanceof LinkCollectionIntent) {
LinkCollectionIntent linksIntent = (LinkCollectionIntent) installable;
return linksIntent.links().contains(link) ||
linksIntent.links().contains(reverseLink);
}
}
}
return false;
}
// Indicates whether the specified links involve the given device.
private boolean pathContainsDevice(Iterable<Link> links, DeviceId id) {
for (Link link : links) {
......
......@@ -192,7 +192,7 @@
// else if we have node selections, deselect them all
// (work already done)
} else if (tls.deselectLink()) {
} else if (tls.deselectAllLinks()) {
// else if we have a link selected, deselect it
// (work already done)
......
......@@ -971,7 +971,7 @@
node: function () { return node; },
zoomingOrPanning: zoomingOrPanning,
updateDeviceColors: td3.updateDeviceColors,
deselectLink: tls.deselectLink
deselectAllLinks: tls.deselectAllLinks
};
}
......
......@@ -31,7 +31,7 @@
network,
showPorts = true, // enable port highlighting by default
enhancedLink = null, // the link over which the mouse is hovering
selectedLink = null; // the link which is currently selected
selectedLinks = {}; // the links which are already selected
// SVG elements;
var svg;
......@@ -210,25 +210,33 @@
function selectLink(ldata) {
// if the new link is same as old link, do nothing
if (selectedLink && ldata && selectedLink.key === ldata.key) return;
// make sure no nodes are selected
tss.deselectAll();
// first, unenhance the currently enhanced link
if (selectedLink) {
unselLink(selectedLink);
}
selectedLink = ldata;
if (selectedLink) {
selLink(selectedLink);
}
if (d3.event.shiftKey && ldata.el.classed('selected')) {
unselLink(ldata);
return;
}
if (d3.event.shiftKey && !ldata.el.classed('selected')) {
selLink(ldata);
return;
}
tss.deselectAll();
if (!ldata.el.classed('selected')) {
selLink(ldata);
return;
}
if (ldata.el.classed('selected')) {
unselLink(ldata);
}
}
function unselLink(d) {
// guard against link element not set
if (d.el) {
d.el.classed('selected', false);
delete selectedLinks[d.key];
}
}
......@@ -237,6 +245,7 @@
if (!d.el) return;
d.el.classed('selected', true);
selectedLinks[d.key] = {key : d};
tps.displayLink(d, tov.hooks.modifyLinkData);
tps.displaySomething();
......@@ -252,6 +261,9 @@
function mouseClickHandler() {
var mp, link, node;
if (!d3.event.shiftKey) {
deselectAllLinks();
}
if (!tss.clickConsumed()) {
mp = getLogicalMousePosition(this);
......@@ -262,6 +274,7 @@
} else {
link = computeNearestLink(mp);
selectLink(link);
tss.selectObject(link);
}
}
}
......@@ -285,13 +298,15 @@
return on;
}
function deselectLink() {
if (selectedLink) {
unselLink(selectedLink);
selectedLink = null;
return true;
function deselectAllLinks() {
if (Object.keys(selectedLinks).length > 0) {
network.links.forEach(function (d) {
if (selectedLinks[d.key]) {
unselLink(d);
}
});
}
return false;
}
// ==========================
......@@ -333,7 +348,7 @@
initLink: initLink,
destroyLink: destroyLink,
togglePorts: togglePorts,
deselectLink: deselectLink
deselectAllLinks: deselectAllLinks
};
}]);
}());
......
......@@ -264,7 +264,7 @@
table = detail.appendBody('table'),
tbody = table.append('tbody');
title.text('Selected Nodes');
title.text('Selected Items');
ids.forEach(function (d, i) {
addProp(tbody, i+1, d);
});
......
......@@ -31,7 +31,7 @@
node() // get ref to D3 selection of nodes
zoomingOrPanning( ev )
updateDeviceColors( [dev] )
deselectLink()
deselectAllLinks()
*/
// internal state
......@@ -106,12 +106,27 @@
}
});
}
if (!n) return;
if (obj.class === 'link') {
if (selections[obj.key]) {
deselectObject(obj.key);
} else {
selections[obj.key] = { obj: obj, el: el };
selectOrder.push(obj.key);
}
updateDetail();
return;
}
if (!n) {
return;
}
if (nodeEv) {
consumeClick = true;
}
api.deselectLink();
if (ev.shiftKey && n.classed('selected')) {
deselectObject(obj.id);
......@@ -196,6 +211,11 @@
function singleSelect() {
var data = getSel(0).obj;
//the link details are already taken care of in topoLink.js
if (data.class === 'link') {
return;
}
requestDetails(data);
// NOTE: detail panel is shown as a response to receiving
// a 'showDetails' event from the server. See 'showDetails'
......
......@@ -75,8 +75,10 @@
var hov = api.hovered();
function hoverValid() {
return hoverMode === 'intents' &&
hov && (hov.class === 'host' || hov.class === 'device');
return hoverMode === 'intents' && hov && (
hov.class === 'host' ||
hov.class === 'device' ||
hov.class === 'link');
}
if (api.somethingSelected()) {
......