Committed by
Gerrit Code Review
GUI fixes/breaks.
Change-Id: Ic5c8b087cc32506162153b2756a677c7d9e3bdd7
Showing
6 changed files
with
258 additions
and
95 deletions
... | @@ -66,7 +66,7 @@ import static org.slf4j.LoggerFactory.getLogger; | ... | @@ -66,7 +66,7 @@ import static org.slf4j.LoggerFactory.getLogger; |
66 | public class DefaultTopologyProvider extends AbstractProvider | 66 | public class DefaultTopologyProvider extends AbstractProvider |
67 | implements TopologyProvider { | 67 | implements TopologyProvider { |
68 | 68 | ||
69 | - private static final int MAX_THREADS = 8; | 69 | + private static final int MAX_THREADS = 32; |
70 | private static final int DEFAULT_MAX_EVENTS = 1000; | 70 | private static final int DEFAULT_MAX_EVENTS = 1000; |
71 | private static final int DEFAULT_MAX_IDLE_MS = 10; | 71 | private static final int DEFAULT_MAX_IDLE_MS = 10; |
72 | private static final int DEFAULT_MAX_BATCH_MS = 50; | 72 | private static final int DEFAULT_MAX_BATCH_MS = 50; | ... | ... |
tools/test/topos/topo-200sw-linkalarm.py
0 → 100644
1 | + | ||
2 | +from mininet.topo import Topo | ||
3 | + | ||
4 | +class MyTopo( Topo ): | ||
5 | + "10 'floating' switch topology" | ||
6 | + | ||
7 | + def __init__( self ): | ||
8 | + # Initialize topology | ||
9 | + Topo.__init__( self ) | ||
10 | + | ||
11 | + sw_list = [] | ||
12 | + swC = self.addSwitch('sc', dpid = 'ffffffff00000001') | ||
13 | + | ||
14 | + for i in range(1, 201): | ||
15 | + switch=self.addSwitch('s'+str(i), dpid = str(i).zfill(16)) | ||
16 | + self.addLink(switch,swC) | ||
17 | + | ||
18 | + sw_list.append(switch) | ||
19 | + | ||
20 | +topos = { 'mytopo': ( lambda: MyTopo() ) } |
... | @@ -33,6 +33,7 @@ import org.onlab.onos.net.intent.PathIntent; | ... | @@ -33,6 +33,7 @@ import org.onlab.onos.net.intent.PathIntent; |
33 | import org.onlab.onos.net.intent.PointToPointIntent; | 33 | import org.onlab.onos.net.intent.PointToPointIntent; |
34 | import org.onlab.onos.net.link.LinkService; | 34 | import org.onlab.onos.net.link.LinkService; |
35 | 35 | ||
36 | +import java.util.ArrayList; | ||
36 | import java.util.HashSet; | 37 | import java.util.HashSet; |
37 | import java.util.List; | 38 | import java.util.List; |
38 | import java.util.Set; | 39 | import java.util.Set; |
... | @@ -71,17 +72,19 @@ public class TopologyViewIntentFilter { | ... | @@ -71,17 +72,19 @@ public class TopologyViewIntentFilter { |
71 | * Finds all path (host-to-host or point-to-point) intents that pertains | 72 | * Finds all path (host-to-host or point-to-point) intents that pertains |
72 | * to the given hosts. | 73 | * to the given hosts. |
73 | * | 74 | * |
74 | - * @param hosts set of hosts to query by | 75 | + * @param hosts set of hosts to query by |
75 | - * @param devices set of devices to query by | 76 | + * @param devices set of devices to query by |
77 | + * @param sourceIntents collection of intents to search | ||
76 | * @return set of intents that 'match' all hosts and devices given | 78 | * @return set of intents that 'match' all hosts and devices given |
77 | */ | 79 | */ |
78 | - Set<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices) { | 80 | + List<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices, |
81 | + Iterable<Intent> sourceIntents) { | ||
79 | // Derive from this the set of edge connect points. | 82 | // Derive from this the set of edge connect points. |
80 | Set<ConnectPoint> edgePoints = getEdgePoints(hosts); | 83 | Set<ConnectPoint> edgePoints = getEdgePoints(hosts); |
81 | 84 | ||
82 | // Iterate over all intents and produce a set that contains only those | 85 | // Iterate over all intents and produce a set that contains only those |
83 | // intents that target all selected hosts or derived edge connect points. | 86 | // intents that target all selected hosts or derived edge connect points. |
84 | - return getIntents(hosts, devices, edgePoints); | 87 | + return getIntents(hosts, devices, edgePoints, sourceIntents); |
85 | } | 88 | } |
86 | 89 | ||
87 | 90 | ||
... | @@ -94,10 +97,11 @@ public class TopologyViewIntentFilter { | ... | @@ -94,10 +97,11 @@ public class TopologyViewIntentFilter { |
94 | return edgePoints; | 97 | return edgePoints; |
95 | } | 98 | } |
96 | 99 | ||
97 | - // Produces a set of intents that target all selected hosts, devices or connect points. | 100 | + // Produces a list of intents that target all selected hosts, devices or connect points. |
98 | - private Set<Intent> getIntents(Set<Host> hosts, Set<Device> devices, | 101 | + private List<Intent> getIntents(Set<Host> hosts, Set<Device> devices, |
99 | - Set<ConnectPoint> edgePoints) { | 102 | + Set<ConnectPoint> edgePoints, |
100 | - Set<Intent> intents = new HashSet<>(); | 103 | + Iterable<Intent> sourceIntents) { |
104 | + List<Intent> intents = new ArrayList<>(); | ||
101 | if (hosts.isEmpty() && devices.isEmpty()) { | 105 | if (hosts.isEmpty() && devices.isEmpty()) { |
102 | return intents; | 106 | return intents; |
103 | } | 107 | } |
... | @@ -105,7 +109,7 @@ public class TopologyViewIntentFilter { | ... | @@ -105,7 +109,7 @@ public class TopologyViewIntentFilter { |
105 | Set<OpticalConnectivityIntent> opticalIntents = new HashSet<>(); | 109 | Set<OpticalConnectivityIntent> opticalIntents = new HashSet<>(); |
106 | 110 | ||
107 | // Search through all intents and see if they are relevant to our search. | 111 | // Search through all intents and see if they are relevant to our search. |
108 | - for (Intent intent : intentService.getIntents()) { | 112 | + for (Intent intent : sourceIntents) { |
109 | if (intentService.getIntentState(intent.id()) == INSTALLED) { | 113 | if (intentService.getIntentState(intent.id()) == INSTALLED) { |
110 | boolean isRelevant = false; | 114 | boolean isRelevant = false; |
111 | if (intent instanceof HostToHostIntent) { | 115 | if (intent instanceof HostToHostIntent) { |
... | @@ -140,7 +144,7 @@ public class TopologyViewIntentFilter { | ... | @@ -140,7 +144,7 @@ public class TopologyViewIntentFilter { |
140 | } | 144 | } |
141 | 145 | ||
142 | // Indicates whether the specified intent involves all of the given hosts. | 146 | // Indicates whether the specified intent involves all of the given hosts. |
143 | - private boolean isIntentRelevantToHosts(HostToHostIntent intent, Set<Host> hosts) { | 147 | + private boolean isIntentRelevantToHosts(HostToHostIntent intent, Iterable<Host> hosts) { |
144 | for (Host host : hosts) { | 148 | for (Host host : hosts) { |
145 | HostId id = host.id(); | 149 | HostId id = host.id(); |
146 | // Bail if intent does not involve this host. | 150 | // Bail if intent does not involve this host. |
... | @@ -152,7 +156,7 @@ public class TopologyViewIntentFilter { | ... | @@ -152,7 +156,7 @@ public class TopologyViewIntentFilter { |
152 | } | 156 | } |
153 | 157 | ||
154 | // Indicates whether the specified intent involves all of the given devices. | 158 | // Indicates whether the specified intent involves all of the given devices. |
155 | - private boolean isIntentRelevantToDevices(Intent intent, Set<Device> devices) { | 159 | + private boolean isIntentRelevantToDevices(Intent intent, Iterable<Device> devices) { |
156 | List<Intent> installables = intentService.getInstallableIntents(intent.id()); | 160 | List<Intent> installables = intentService.getInstallableIntents(intent.id()); |
157 | for (Device device : devices) { | 161 | for (Device device : devices) { |
158 | if (!isIntentRelevantToDevice(installables, device)) { | 162 | if (!isIntentRelevantToDevice(installables, device)) { |
... | @@ -192,7 +196,8 @@ public class TopologyViewIntentFilter { | ... | @@ -192,7 +196,8 @@ public class TopologyViewIntentFilter { |
192 | return false; | 196 | return false; |
193 | } | 197 | } |
194 | 198 | ||
195 | - private boolean isIntentRelevant(PointToPointIntent intent, Set<ConnectPoint> edgePoints) { | 199 | + private boolean isIntentRelevant(PointToPointIntent intent, |
200 | + Iterable<ConnectPoint> edgePoints) { | ||
196 | for (ConnectPoint point : edgePoints) { | 201 | for (ConnectPoint point : edgePoints) { |
197 | // Bail if intent does not involve this edge point. | 202 | // Bail if intent does not involve this edge point. |
198 | if (!point.equals(intent.egressPoint()) && | 203 | if (!point.equals(intent.egressPoint()) && |
... | @@ -205,7 +210,7 @@ public class TopologyViewIntentFilter { | ... | @@ -205,7 +210,7 @@ public class TopologyViewIntentFilter { |
205 | 210 | ||
206 | // Indicates whether the specified intent involves all of the given edge points. | 211 | // Indicates whether the specified intent involves all of the given edge points. |
207 | private boolean isIntentRelevant(MultiPointToSinglePointIntent intent, | 212 | private boolean isIntentRelevant(MultiPointToSinglePointIntent intent, |
208 | - Set<ConnectPoint> edgePoints) { | 213 | + Iterable<ConnectPoint> edgePoints) { |
209 | for (ConnectPoint point : edgePoints) { | 214 | for (ConnectPoint point : edgePoints) { |
210 | // Bail if intent does not involve this edge point. | 215 | // Bail if intent does not involve this edge point. |
211 | if (!point.equals(intent.egressPoint()) && | 216 | if (!point.equals(intent.egressPoint()) && |
... | @@ -218,7 +223,7 @@ public class TopologyViewIntentFilter { | ... | @@ -218,7 +223,7 @@ public class TopologyViewIntentFilter { |
218 | 223 | ||
219 | // Indicates whether the specified intent involves all of the given edge points. | 224 | // Indicates whether the specified intent involves all of the given edge points. |
220 | private boolean isIntentRelevant(OpticalConnectivityIntent opticalIntent, | 225 | private boolean isIntentRelevant(OpticalConnectivityIntent opticalIntent, |
221 | - Set<Intent> intents) { | 226 | + Iterable<Intent> intents) { |
222 | Link ccSrc = getFirstLink(opticalIntent.getSrc(), false); | 227 | Link ccSrc = getFirstLink(opticalIntent.getSrc(), false); |
223 | Link ccDst = getFirstLink(opticalIntent.getDst(), true); | 228 | Link ccDst = getFirstLink(opticalIntent.getDst(), true); |
224 | 229 | ... | ... |
... | @@ -676,13 +676,16 @@ public abstract class TopologyViewMessages { | ... | @@ -676,13 +676,16 @@ public abstract class TopologyViewMessages { |
676 | List<Intent> installables = intentService.getInstallableIntents(intent.id()); | 676 | List<Intent> installables = intentService.getInstallableIntents(intent.id()); |
677 | if (installables != null) { | 677 | if (installables != null) { |
678 | for (Intent installable : installables) { | 678 | for (Intent installable : installables) { |
679 | - String cls = isOptical ? trafficClass.type + " optical" : trafficClass.type; | 679 | + String type = isOptical ? trafficClass.type + " optical" : trafficClass.type; |
680 | if (installable instanceof PathIntent) { | 680 | if (installable instanceof PathIntent) { |
681 | - classifyLinks(cls, biLinks, ((PathIntent) installable).path().links()); | 681 | + classifyLinks(type, biLinks, trafficClass.showTraffic, |
682 | + ((PathIntent) installable).path().links()); | ||
682 | } else if (installable instanceof LinkCollectionIntent) { | 683 | } else if (installable instanceof LinkCollectionIntent) { |
683 | - classifyLinks(cls, biLinks, ((LinkCollectionIntent) installable).links()); | 684 | + classifyLinks(type, biLinks, trafficClass.showTraffic, |
685 | + ((LinkCollectionIntent) installable).links()); | ||
684 | } else if (installable instanceof OpticalPathIntent) { | 686 | } else if (installable instanceof OpticalPathIntent) { |
685 | - classifyLinks(cls, biLinks, ((OpticalPathIntent) installable).path().links()); | 687 | + classifyLinks(type, biLinks, trafficClass.showTraffic, |
688 | + ((OpticalPathIntent) installable).path().links()); | ||
686 | } | 689 | } |
687 | } | 690 | } |
688 | } | 691 | } |
... | @@ -695,12 +698,14 @@ public abstract class TopologyViewMessages { | ... | @@ -695,12 +698,14 @@ public abstract class TopologyViewMessages { |
695 | // Adds the link segments (path or tree) associated with the specified | 698 | // Adds the link segments (path or tree) associated with the specified |
696 | // connectivity intent | 699 | // connectivity intent |
697 | private void classifyLinks(String type, Map<LinkKey, BiLink> biLinks, | 700 | private void classifyLinks(String type, Map<LinkKey, BiLink> biLinks, |
698 | - Iterable<Link> links) { | 701 | + boolean showTraffic, Iterable<Link> links) { |
699 | if (links != null) { | 702 | if (links != null) { |
700 | for (Link link : links) { | 703 | for (Link link : links) { |
701 | BiLink biLink = addLink(biLinks, link); | 704 | BiLink biLink = addLink(biLinks, link); |
702 | if (isInfrastructureEgress(link)) { | 705 | if (isInfrastructureEgress(link)) { |
703 | - biLink.addLoad(statService.load(link)); | 706 | + if (showTraffic) { |
707 | + biLink.addLoad(statService.load(link)); | ||
708 | + } | ||
704 | biLink.addClass(type); | 709 | biLink.addClass(type); |
705 | } | 710 | } |
706 | } | 711 | } |
... | @@ -862,12 +867,18 @@ public abstract class TopologyViewMessages { | ... | @@ -862,12 +867,18 @@ public abstract class TopologyViewMessages { |
862 | 867 | ||
863 | // Auxiliary carrier of data for requesting traffic message. | 868 | // Auxiliary carrier of data for requesting traffic message. |
864 | protected class TrafficClass { | 869 | protected class TrafficClass { |
870 | + public final boolean showTraffic; | ||
865 | public final String type; | 871 | public final String type; |
866 | - public final Set<Intent> intents; | 872 | + public final Iterable<Intent> intents; |
873 | + | ||
874 | + TrafficClass(String type, Iterable<Intent> intents) { | ||
875 | + this(type, intents, false); | ||
876 | + } | ||
867 | 877 | ||
868 | - TrafficClass(String type, Set<Intent> intents) { | 878 | + TrafficClass(String type, Iterable<Intent> intents, boolean showTraffic) { |
869 | this.type = type; | 879 | this.type = type; |
870 | this.intents = intents; | 880 | this.intents = intents; |
881 | + this.showTraffic = showTraffic; | ||
871 | } | 882 | } |
872 | } | 883 | } |
873 | 884 | ... | ... |
... | @@ -90,7 +90,8 @@ public class TopologyViewWebSocket | ... | @@ -90,7 +90,8 @@ public class TopologyViewWebSocket |
90 | 90 | ||
91 | private static final String APP_ID = "org.onlab.onos.gui"; | 91 | private static final String APP_ID = "org.onlab.onos.gui"; |
92 | 92 | ||
93 | - private static final long TRAFFIC_FREQUENCY_SEC = 2000; | 93 | + private static final long TRAFFIC_FREQUENCY = 2000; |
94 | + private static final long SUMMARY_FREQUENCY = 30000; | ||
94 | 95 | ||
95 | private static final Comparator<? super ControllerNode> NODE_COMPARATOR = | 96 | private static final Comparator<? super ControllerNode> NODE_COMPARATOR = |
96 | new Comparator<ControllerNode>() { | 97 | new Comparator<ControllerNode>() { |
... | @@ -103,9 +104,9 @@ public class TopologyViewWebSocket | ... | @@ -103,9 +104,9 @@ public class TopologyViewWebSocket |
103 | 104 | ||
104 | private final Timer timer = new Timer("topology-view"); | 105 | private final Timer timer = new Timer("topology-view"); |
105 | 106 | ||
106 | - private static final int MAX_EVENTS = 500; | 107 | + private static final int MAX_EVENTS = 1000; |
107 | - private static final int MAX_BATCH_MS = 1000; | 108 | + private static final int MAX_BATCH_MS = 5000; |
108 | - private static final int MAX_IDLE_MS = 500; | 109 | + private static final int MAX_IDLE_MS = 1000; |
109 | 110 | ||
110 | private final ApplicationId appId; | 111 | private final ApplicationId appId; |
111 | 112 | ||
... | @@ -122,15 +123,23 @@ public class TopologyViewWebSocket | ... | @@ -122,15 +123,23 @@ public class TopologyViewWebSocket |
122 | 123 | ||
123 | private final EventAccumulator eventAccummulator = new InternalEventAccummulator(); | 124 | private final EventAccumulator eventAccummulator = new InternalEventAccummulator(); |
124 | 125 | ||
125 | - private boolean summaryEnabled = true; | ||
126 | private TimerTask trafficTask; | 126 | private TimerTask trafficTask; |
127 | private ObjectNode trafficEvent; | 127 | private ObjectNode trafficEvent; |
128 | 128 | ||
129 | + private TimerTask summaryTask; | ||
130 | + private ObjectNode summaryEvent; | ||
131 | + | ||
129 | private long lastActive = System.currentTimeMillis(); | 132 | private long lastActive = System.currentTimeMillis(); |
130 | private boolean listenersRemoved = false; | 133 | private boolean listenersRemoved = false; |
131 | 134 | ||
132 | private TopologyViewIntentFilter intentFilter; | 135 | private TopologyViewIntentFilter intentFilter; |
133 | 136 | ||
137 | + // Current selection context | ||
138 | + private Set<Host> selectedHosts; | ||
139 | + private Set<Device> selectedDevices; | ||
140 | + private List<Intent> selectedIntents; | ||
141 | + private int currentIntentIndex = -1; | ||
142 | + | ||
134 | /** | 143 | /** |
135 | * Creates a new web-socket for serving data to GUI topology view. | 144 | * Creates a new web-socket for serving data to GUI topology view. |
136 | * | 145 | * |
... | @@ -204,7 +213,6 @@ public class TopologyViewWebSocket | ... | @@ -204,7 +213,6 @@ public class TopologyViewWebSocket |
204 | processMessage((ObjectNode) mapper.reader().readTree(data)); | 213 | processMessage((ObjectNode) mapper.reader().readTree(data)); |
205 | } catch (Exception e) { | 214 | } catch (Exception e) { |
206 | log.warn("Unable to parse GUI request {} due to {}", data, e); | 215 | log.warn("Unable to parse GUI request {} due to {}", data, e); |
207 | - log.warn("Boom!!!!", e); | ||
208 | } | 216 | } |
209 | } | 217 | } |
210 | 218 | ||
... | @@ -221,19 +229,29 @@ public class TopologyViewWebSocket | ... | @@ -221,19 +229,29 @@ public class TopologyViewWebSocket |
221 | } else if (type.equals("addMultiSourceIntent")) { | 229 | } else if (type.equals("addMultiSourceIntent")) { |
222 | createMultiSourceIntent(event); | 230 | createMultiSourceIntent(event); |
223 | 231 | ||
224 | - } else if (type.equals("requestTraffic")) { | 232 | + } else if (type.equals("requestRelatedIntents")) { |
225 | - requestTraffic(event); | 233 | + requestRelatedIntents(event); |
234 | + } else if (type.equals("requestNextRelatedIntent")) { | ||
235 | + requestNextRelatedIntent(event); | ||
236 | + } else if (type.equals("requestSelectedIntentTraffic")) { | ||
237 | + requestSelectedIntentTraffic(event); | ||
238 | + | ||
226 | } else if (type.equals("requestAllTraffic")) { | 239 | } else if (type.equals("requestAllTraffic")) { |
227 | requestAllTraffic(event); | 240 | requestAllTraffic(event); |
241 | + startTrafficMonitoring(event); | ||
242 | + | ||
228 | } else if (type.equals("requestDeviceLinkFlows")) { | 243 | } else if (type.equals("requestDeviceLinkFlows")) { |
229 | requestDeviceLinkFlows(event); | 244 | requestDeviceLinkFlows(event); |
245 | + startTrafficMonitoring(event); | ||
246 | + | ||
230 | } else if (type.equals("cancelTraffic")) { | 247 | } else if (type.equals("cancelTraffic")) { |
231 | cancelTraffic(event); | 248 | cancelTraffic(event); |
232 | 249 | ||
233 | } else if (type.equals("requestSummary")) { | 250 | } else if (type.equals("requestSummary")) { |
234 | requestSummary(event); | 251 | requestSummary(event); |
252 | + startSummaryMonitoring(event); | ||
235 | } else if (type.equals("cancelSummary")) { | 253 | } else if (type.equals("cancelSummary")) { |
236 | - cancelSummary(event); | 254 | + stopSummaryMonitoring(); |
237 | 255 | ||
238 | } else if (type.equals("equalizeMasters")) { | 256 | } else if (type.equals("equalizeMasters")) { |
239 | equalizeMasters(event); | 257 | equalizeMasters(event); |
... | @@ -324,8 +342,9 @@ public class TopologyViewWebSocket | ... | @@ -324,8 +342,9 @@ public class TopologyViewWebSocket |
324 | new HostToHostIntent(appId, one, two, | 342 | new HostToHostIntent(appId, one, two, |
325 | DefaultTrafficSelector.builder().build(), | 343 | DefaultTrafficSelector.builder().build(), |
326 | DefaultTrafficTreatment.builder().build()); | 344 | DefaultTrafficTreatment.builder().build()); |
327 | - startMonitoring(event); | 345 | + |
328 | intentService.submit(intent); | 346 | intentService.submit(intent); |
347 | + startMonitoringIntent(event, intent); | ||
329 | } | 348 | } |
330 | 349 | ||
331 | // Creates multi-source-to-single-dest intent. | 350 | // Creates multi-source-to-single-dest intent. |
... | @@ -348,10 +367,24 @@ public class TopologyViewWebSocket | ... | @@ -348,10 +367,24 @@ public class TopologyViewWebSocket |
348 | MultiPointToSinglePointIntent intent = | 367 | MultiPointToSinglePointIntent intent = |
349 | new MultiPointToSinglePointIntent(appId, selector, treatment, | 368 | new MultiPointToSinglePointIntent(appId, selector, treatment, |
350 | ingressPoints, dstHost.location()); | 369 | ingressPoints, dstHost.location()); |
351 | - startMonitoring(event); | 370 | + |
352 | intentService.submit(intent); | 371 | intentService.submit(intent); |
372 | + startMonitoringIntent(event, intent); | ||
353 | } | 373 | } |
354 | 374 | ||
375 | + | ||
376 | + private synchronized void startMonitoringIntent(ObjectNode event, Intent intent) { | ||
377 | + selectedHosts = new HashSet<>(); | ||
378 | + selectedDevices = new HashSet<>(); | ||
379 | + selectedIntents = new ArrayList<>(); | ||
380 | + selectedIntents.add(intent); | ||
381 | + currentIntentIndex = -1; | ||
382 | + requestNextRelatedIntent(event); | ||
383 | + requestSelectedIntentTraffic(event); | ||
384 | + } | ||
385 | + | ||
386 | + | ||
387 | + | ||
355 | private Set<ConnectPoint> getHostLocations(Set<HostId> hostIds) { | 388 | private Set<ConnectPoint> getHostLocations(Set<HostId> hostIds) { |
356 | Set<ConnectPoint> points = new HashSet<>(); | 389 | Set<ConnectPoint> points = new HashSet<>(); |
357 | for (HostId hostId : hostIds) { | 390 | for (HostId hostId : hostIds) { |
... | @@ -374,17 +407,15 @@ public class TopologyViewWebSocket | ... | @@ -374,17 +407,15 @@ public class TopologyViewWebSocket |
374 | } | 407 | } |
375 | 408 | ||
376 | 409 | ||
377 | - private synchronized long startMonitoring(ObjectNode event) { | 410 | + private synchronized long startTrafficMonitoring(ObjectNode event) { |
378 | - if (trafficTask != null) { | 411 | + stopTrafficMonitoring(); |
379 | - stopMonitoring(); | ||
380 | - } | ||
381 | trafficEvent = event; | 412 | trafficEvent = event; |
382 | trafficTask = new TrafficMonitor(); | 413 | trafficTask = new TrafficMonitor(); |
383 | - timer.schedule(trafficTask, TRAFFIC_FREQUENCY_SEC, TRAFFIC_FREQUENCY_SEC); | 414 | + timer.schedule(trafficTask, TRAFFIC_FREQUENCY, TRAFFIC_FREQUENCY); |
384 | return number(event, "sid"); | 415 | return number(event, "sid"); |
385 | } | 416 | } |
386 | 417 | ||
387 | - private synchronized void stopMonitoring() { | 418 | + private synchronized void stopTrafficMonitoring() { |
388 | if (trafficTask != null) { | 419 | if (trafficTask != null) { |
389 | trafficTask.cancel(); | 420 | trafficTask.cancel(); |
390 | trafficTask = null; | 421 | trafficTask = null; |
... | @@ -394,13 +425,13 @@ public class TopologyViewWebSocket | ... | @@ -394,13 +425,13 @@ public class TopologyViewWebSocket |
394 | 425 | ||
395 | // Subscribes for host traffic messages. | 426 | // Subscribes for host traffic messages. |
396 | private synchronized void requestAllTraffic(ObjectNode event) { | 427 | private synchronized void requestAllTraffic(ObjectNode event) { |
397 | - long sid = startMonitoring(event); | 428 | + long sid = startTrafficMonitoring(event); |
398 | sendMessage(trafficSummaryMessage(sid)); | 429 | sendMessage(trafficSummaryMessage(sid)); |
399 | } | 430 | } |
400 | 431 | ||
401 | private void requestDeviceLinkFlows(ObjectNode event) { | 432 | private void requestDeviceLinkFlows(ObjectNode event) { |
402 | ObjectNode payload = payload(event); | 433 | ObjectNode payload = payload(event); |
403 | - long sid = startMonitoring(event); | 434 | + long sid = startTrafficMonitoring(event); |
404 | 435 | ||
405 | // Get the set of selected hosts and their intents. | 436 | // Get the set of selected hosts and their intents. |
406 | ArrayNode ids = (ArrayNode) payload.path("ids"); | 437 | ArrayNode ids = (ArrayNode) payload.path("ids"); |
... | @@ -416,58 +447,122 @@ public class TopologyViewWebSocket | ... | @@ -416,58 +447,122 @@ public class TopologyViewWebSocket |
416 | } | 447 | } |
417 | 448 | ||
418 | 449 | ||
419 | - // Subscribes for host traffic messages. | 450 | + // Requests related intents message. |
420 | - private synchronized void requestTraffic(ObjectNode event) { | 451 | + private synchronized void requestRelatedIntents(ObjectNode event) { |
421 | ObjectNode payload = payload(event); | 452 | ObjectNode payload = payload(event); |
422 | if (!payload.has("ids")) { | 453 | if (!payload.has("ids")) { |
423 | return; | 454 | return; |
424 | } | 455 | } |
425 | 456 | ||
426 | - long sid = startMonitoring(event); | 457 | + long sid = number(event, "sid"); |
427 | 458 | ||
428 | - // Get the set of selected hosts and their intents. | 459 | + // Cancel any other traffic monitoring mode. |
429 | - ArrayNode ids = (ArrayNode) payload.path("ids"); | 460 | + stopTrafficMonitoring(); |
430 | - Set<Host> hosts = getHosts(ids); | ||
431 | - Set<Device> devices = getDevices(ids); | ||
432 | - Set<Intent> intents = intentFilter.findPathIntents(hosts, devices); | ||
433 | 461 | ||
434 | - // If there is a hover node, include it in the hosts and find intents. | ||
435 | String hover = string(payload, "hover"); | 462 | String hover = string(payload, "hover"); |
436 | - Set<Intent> hoverIntents; | 463 | + if (haveSelectedIntents()) { |
464 | + // Get the set of selected hosts and their intents. | ||
465 | + ArrayNode ids = (ArrayNode) payload.path("ids"); | ||
466 | + selectedHosts = getHosts(ids); | ||
467 | + selectedDevices = getDevices(ids); | ||
468 | + selectedIntents = intentFilter.findPathIntents(selectedHosts, selectedDevices, | ||
469 | + intentService.getIntents()); | ||
470 | + currentIntentIndex = -1; | ||
471 | + | ||
472 | + // Send a message to highlight all links of all monitored intents. | ||
473 | + sendMessage(trafficMessage(sid, new TrafficClass("primary", selectedIntents))); | ||
474 | + } | ||
475 | + | ||
437 | if (!isNullOrEmpty(hover)) { | 476 | if (!isNullOrEmpty(hover)) { |
438 | - addHover(hosts, devices, hover); | 477 | + // If there is a hover node, include it in the selection and find intents. |
439 | - hoverIntents = intentFilter.findPathIntents(hosts, devices); | 478 | + processExtendedSelection(sid, hover); |
440 | - intents.removeAll(hoverIntents); | 479 | + } |
480 | + } | ||
481 | + | ||
482 | + private boolean haveSelectedIntents() { | ||
483 | + return selectedIntents != null && !selectedIntents.isEmpty(); | ||
484 | + } | ||
485 | + | ||
486 | + private void processExtendedSelection(long sid, String hover) { | ||
487 | + Set<Host> hoverSelHosts = new HashSet<>(selectedHosts); | ||
488 | + Set<Device> hoverSelDevices = new HashSet<>(selectedDevices); | ||
489 | + addHover(hoverSelHosts, hoverSelDevices, hover); | ||
441 | 490 | ||
442 | - // Send an initial message to highlight all links of all monitored intents. | 491 | + List<Intent> primary = |
443 | - sendMessage(trafficMessage(sid, | 492 | + intentFilter.findPathIntents(hoverSelHosts, hoverSelDevices, |
444 | - new TrafficClass("primary", hoverIntents), | 493 | + selectedIntents); |
445 | - new TrafficClass("secondary", intents))); | 494 | + Set<Intent> secondary = new HashSet<>(selectedIntents); |
495 | + secondary.removeAll(primary); | ||
446 | 496 | ||
447 | - } else { | 497 | + // Send a message to highlight all links of all monitored intents. |
448 | - // Send an initial message to highlight all links of all monitored intents. | 498 | + sendMessage(trafficMessage(sid, new TrafficClass("primary", primary), |
449 | - sendMessage(trafficMessage(sid, new TrafficClass("primary", intents))); | 499 | + new TrafficClass("secondary", secondary))); |
500 | + } | ||
501 | + | ||
502 | + // Requests next of the related intents. | ||
503 | + private void requestNextRelatedIntent(ObjectNode event) { | ||
504 | + if (haveSelectedIntents()) { | ||
505 | + currentIntentIndex = (currentIntentIndex + 1) % selectedIntents.size(); | ||
506 | + Intent selectedIntent = selectedIntents.get(currentIntentIndex); | ||
507 | + log.info("Requested next intent {}", selectedIntent.id()); | ||
508 | + | ||
509 | + Set<Intent> primary = new HashSet<>(); | ||
510 | + primary.add(selectedIntent); | ||
511 | + | ||
512 | + Set<Intent> secondary = new HashSet<>(selectedIntents); | ||
513 | + secondary.remove(selectedIntent); | ||
514 | + | ||
515 | + // Send a message to highlight all links of the selected intent. | ||
516 | + sendMessage(trafficMessage(number(event, "sid"), | ||
517 | + new TrafficClass("primary", primary), | ||
518 | + new TrafficClass("secondary", secondary))); | ||
519 | + } | ||
520 | + } | ||
521 | + | ||
522 | + // Requests monitoring of traffic for the selected intent. | ||
523 | + private void requestSelectedIntentTraffic(ObjectNode event) { | ||
524 | + if (haveSelectedIntents()) { | ||
525 | + Intent selectedIntent = selectedIntents.get(currentIntentIndex); | ||
526 | + log.info("Requested traffic for selected {}", selectedIntent.id()); | ||
527 | + | ||
528 | + Set<Intent> primary = new HashSet<>(); | ||
529 | + primary.add(selectedIntent); | ||
530 | + | ||
531 | + // Send a message to highlight all links of the selected intent. | ||
532 | + sendMessage(trafficMessage(number(event, "sid"), | ||
533 | + new TrafficClass("primary", primary, true))); | ||
450 | } | 534 | } |
451 | } | 535 | } |
452 | 536 | ||
453 | // Cancels sending traffic messages. | 537 | // Cancels sending traffic messages. |
454 | private void cancelTraffic(ObjectNode event) { | 538 | private void cancelTraffic(ObjectNode event) { |
539 | + selectedIntents = null; | ||
455 | sendMessage(trafficMessage(number(event, "sid"))); | 540 | sendMessage(trafficMessage(number(event, "sid"))); |
456 | - stopMonitoring(); | 541 | + stopTrafficMonitoring(); |
542 | + } | ||
543 | + | ||
544 | + | ||
545 | + private synchronized long startSummaryMonitoring(ObjectNode event) { | ||
546 | + stopSummaryMonitoring(); | ||
547 | + summaryEvent = event; | ||
548 | + summaryTask = new SummaryMonitor(); | ||
549 | + timer.schedule(summaryTask, SUMMARY_FREQUENCY, SUMMARY_FREQUENCY); | ||
550 | + return number(event, "sid"); | ||
457 | } | 551 | } |
458 | 552 | ||
553 | + private synchronized void stopSummaryMonitoring() { | ||
554 | + if (summaryEvent != null) { | ||
555 | + summaryTask.cancel(); | ||
556 | + summaryTask = null; | ||
557 | + summaryEvent = null; | ||
558 | + } | ||
559 | + } | ||
459 | 560 | ||
460 | // Subscribes for summary messages. | 561 | // Subscribes for summary messages. |
461 | private synchronized void requestSummary(ObjectNode event) { | 562 | private synchronized void requestSummary(ObjectNode event) { |
462 | - summaryEnabled = true; | ||
463 | sendMessage(summmaryMessage(number(event, "sid"))); | 563 | sendMessage(summmaryMessage(number(event, "sid"))); |
464 | } | 564 | } |
465 | 565 | ||
466 | - // Cancels sending summary messages. | ||
467 | - private synchronized void cancelSummary(ObjectNode event) { | ||
468 | - summaryEnabled = false; | ||
469 | - } | ||
470 | - | ||
471 | 566 | ||
472 | // Forces mastership role rebalancing. | 567 | // Forces mastership role rebalancing. |
473 | private void equalizeMasters(ObjectNode event) { | 568 | private void equalizeMasters(ObjectNode event) { |
... | @@ -550,7 +645,7 @@ public class TopologyViewWebSocket | ... | @@ -550,7 +645,7 @@ public class TopologyViewWebSocket |
550 | @Override | 645 | @Override |
551 | public void event(IntentEvent event) { | 646 | public void event(IntentEvent event) { |
552 | if (trafficEvent != null) { | 647 | if (trafficEvent != null) { |
553 | - requestTraffic(trafficEvent); | 648 | + requestSelectedIntentTraffic(trafficEvent); |
554 | } | 649 | } |
555 | eventAccummulator.add(event); | 650 | eventAccummulator.add(event); |
556 | } | 651 | } |
... | @@ -564,6 +659,7 @@ public class TopologyViewWebSocket | ... | @@ -564,6 +659,7 @@ public class TopologyViewWebSocket |
564 | } | 659 | } |
565 | } | 660 | } |
566 | 661 | ||
662 | + // Periodic update of the traffic information | ||
567 | private class TrafficMonitor extends TimerTask { | 663 | private class TrafficMonitor extends TimerTask { |
568 | @Override | 664 | @Override |
569 | public void run() { | 665 | public void run() { |
... | @@ -574,8 +670,8 @@ public class TopologyViewWebSocket | ... | @@ -574,8 +670,8 @@ public class TopologyViewWebSocket |
574 | requestAllTraffic(trafficEvent); | 670 | requestAllTraffic(trafficEvent); |
575 | } else if (type.equals("requestDeviceLinkFlows")) { | 671 | } else if (type.equals("requestDeviceLinkFlows")) { |
576 | requestDeviceLinkFlows(trafficEvent); | 672 | requestDeviceLinkFlows(trafficEvent); |
577 | - } else { | 673 | + } else if (type.equals("requestSelectedIntentTraffic")) { |
578 | - requestTraffic(trafficEvent); | 674 | + requestSelectedIntentTraffic(trafficEvent); |
579 | } | 675 | } |
580 | } | 676 | } |
581 | } catch (Exception e) { | 677 | } catch (Exception e) { |
... | @@ -585,6 +681,20 @@ public class TopologyViewWebSocket | ... | @@ -585,6 +681,20 @@ public class TopologyViewWebSocket |
585 | } | 681 | } |
586 | } | 682 | } |
587 | 683 | ||
684 | + // Periodic update of the summary information | ||
685 | + private class SummaryMonitor extends TimerTask { | ||
686 | + @Override | ||
687 | + public void run() { | ||
688 | + try { | ||
689 | + if (summaryEvent != null) { | ||
690 | + requestSummary(summaryEvent); | ||
691 | + } | ||
692 | + } catch (Exception e) { | ||
693 | + log.warn("Unable to handle summary request due to {}", e.getMessage()); | ||
694 | + } | ||
695 | + } | ||
696 | + } | ||
697 | + | ||
588 | // Accumulates events to drive methodic update of the summary pane. | 698 | // Accumulates events to drive methodic update of the summary pane. |
589 | private class InternalEventAccummulator extends AbstractEventAccumulator { | 699 | private class InternalEventAccummulator extends AbstractEventAccumulator { |
590 | protected InternalEventAccummulator() { | 700 | protected InternalEventAccummulator() { |
... | @@ -594,7 +704,7 @@ public class TopologyViewWebSocket | ... | @@ -594,7 +704,7 @@ public class TopologyViewWebSocket |
594 | @Override | 704 | @Override |
595 | public void processEvents(List<Event> events) { | 705 | public void processEvents(List<Event> events) { |
596 | try { | 706 | try { |
597 | - if (summaryEnabled) { | 707 | + if (summaryEvent != null) { |
598 | sendMessage(summmaryMessage(0)); | 708 | sendMessage(summmaryMessage(0)); |
599 | } | 709 | } |
600 | } catch (Exception e) { | 710 | } catch (Exception e) { | ... | ... |
... | @@ -145,8 +145,10 @@ | ... | @@ -145,8 +145,10 @@ |
145 | P: togglePorts, | 145 | P: togglePorts, |
146 | U: [unpin, 'Unpin node (hover mouse over)'], | 146 | U: [unpin, 'Unpin node (hover mouse over)'], |
147 | R: [resetPanZoom, 'Reset pan / zoom'], | 147 | R: [resetPanZoom, 'Reset pan / zoom'], |
148 | - V: [showTrafficAction, 'Show related traffic'], | 148 | + V: [showRelatedIntentsAction, 'Show all related intents'], |
149 | - A: [showAllTrafficAction, 'Show all traffic'], | 149 | + N: [showNextIntentAction, 'Show next related intent'], |
150 | + W: [showSelectedIntentTrafficAction, 'Monitor traffic of selected intent'], | ||
151 | + A: [showAllTrafficAction, 'Monitor all traffic'], | ||
150 | F: [showDeviceLinkFlowsAction, 'Show device link flows'], | 152 | F: [showDeviceLinkFlowsAction, 'Show device link flows'], |
151 | X: [toggleNodeLock, 'Lock / unlock node positions'], | 153 | X: [toggleNodeLock, 'Lock / unlock node positions'], |
152 | Z: [toggleOblique, 'Toggle oblique view (Experimental)'], | 154 | Z: [toggleOblique, 'Toggle oblique view (Experimental)'], |
... | @@ -209,10 +211,11 @@ | ... | @@ -209,10 +211,11 @@ |
209 | oblique = false; | 211 | oblique = false; |
210 | 212 | ||
211 | // constants | 213 | // constants |
212 | - var hoverModeAll = 1, | 214 | + var hoverModeNone = 0, |
215 | + hoverModeAll = 1, | ||
213 | hoverModeFlows = 2, | 216 | hoverModeFlows = 2, |
214 | hoverModeIntents = 3, | 217 | hoverModeIntents = 3, |
215 | - hoverMode = hoverModeFlows; | 218 | + hoverMode = hoverModeNone; |
216 | 219 | ||
217 | // D3 selections | 220 | // D3 selections |
218 | var svg, | 221 | var svg, |
... | @@ -394,7 +397,7 @@ | ... | @@ -394,7 +397,7 @@ |
394 | cancelSummary(); | 397 | cancelSummary(); |
395 | stopAntTimer(); | 398 | stopAntTimer(); |
396 | } else { | 399 | } else { |
397 | - hoverMode = hoverModeFlows; | 400 | + hoverMode = hoverModeNone; |
398 | } | 401 | } |
399 | } | 402 | } |
400 | 403 | ||
... | @@ -1219,22 +1222,20 @@ | ... | @@ -1219,22 +1222,20 @@ |
1219 | } | 1222 | } |
1220 | 1223 | ||
1221 | function requestTrafficForMode() { | 1224 | function requestTrafficForMode() { |
1222 | - if (hoverMode === hoverModeAll) { | 1225 | + if (hoverMode === hoverModeFlows) { |
1223 | - requestAllTraffic(); | ||
1224 | - } else if (hoverMode === hoverModeFlows) { | ||
1225 | requestDeviceLinkFlows(); | 1226 | requestDeviceLinkFlows(); |
1226 | } else if (hoverMode === hoverModeIntents) { | 1227 | } else if (hoverMode === hoverModeIntents) { |
1227 | - requestSelectTraffic(); | 1228 | + requestRelatedIntents(); |
1228 | } | 1229 | } |
1229 | } | 1230 | } |
1230 | 1231 | ||
1231 | - function showTrafficAction() { | 1232 | + function showRelatedIntentsAction() { |
1232 | hoverMode = hoverModeIntents; | 1233 | hoverMode = hoverModeIntents; |
1233 | - requestSelectTraffic(); | 1234 | + requestRelatedIntents(); |
1234 | - flash('Related Traffic'); | 1235 | + flash('Related intents'); |
1235 | } | 1236 | } |
1236 | 1237 | ||
1237 | - function requestSelectTraffic() { | 1238 | + function requestRelatedIntents() { |
1238 | function hoverValid() { | 1239 | function hoverValid() { |
1239 | return hoverMode === hoverModeIntents && | 1240 | return hoverMode === hoverModeIntents && |
1240 | hovered && | 1241 | hovered && |
... | @@ -1242,13 +1243,24 @@ | ... | @@ -1242,13 +1243,24 @@ |
1242 | } | 1243 | } |
1243 | 1244 | ||
1244 | if (validateSelectionContext()) { | 1245 | if (validateSelectionContext()) { |
1245 | - sendMessage('requestTraffic', { | 1246 | + sendMessage('requestRelatedIntents', { |
1246 | ids: selectOrder, | 1247 | ids: selectOrder, |
1247 | hover: hoverValid() ? hovered.id : '' | 1248 | hover: hoverValid() ? hovered.id : '' |
1248 | }); | 1249 | }); |
1249 | } | 1250 | } |
1250 | } | 1251 | } |
1251 | 1252 | ||
1253 | + function showNextIntentAction() { | ||
1254 | + hoverMode = hoverModeNone; | ||
1255 | + sendMessage('requestNextRelatedIntent', {}); | ||
1256 | + flash('Next related intent'); | ||
1257 | + } | ||
1258 | + | ||
1259 | + function showSelectedIntentTrafficAction() { | ||
1260 | + hoverMode = hoverModeNone; | ||
1261 | + sendMessage('requestSelectedIntentTraffic', {}); | ||
1262 | + flash('Monitoring selected intent'); | ||
1263 | + } | ||
1252 | 1264 | ||
1253 | function showDeviceLinkFlowsAction() { | 1265 | function showDeviceLinkFlowsAction() { |
1254 | hoverMode = hoverModeFlows; | 1266 | hoverMode = hoverModeFlows; |
... | @@ -2010,13 +2022,17 @@ | ... | @@ -2010,13 +2022,17 @@ |
2010 | } | 2022 | } |
2011 | 2023 | ||
2012 | function nodeMouseOver(d) { | 2024 | function nodeMouseOver(d) { |
2013 | - hovered = d; | 2025 | + if (hovered != d) { |
2014 | - requestTrafficForMode(); | 2026 | + hovered = d; |
2027 | + requestTrafficForMode(); | ||
2028 | + } | ||
2015 | } | 2029 | } |
2016 | 2030 | ||
2017 | function nodeMouseOut(d) { | 2031 | function nodeMouseOut(d) { |
2018 | - hovered = null; | 2032 | + if (hovered != null) { |
2019 | - requestTrafficForMode(); | 2033 | + hovered = null; |
2034 | + requestTrafficForMode(); | ||
2035 | + } | ||
2020 | } | 2036 | } |
2021 | 2037 | ||
2022 | function addHostIcon(node, radius, iid) { | 2038 | function addHostIcon(node, radius, iid) { |
... | @@ -2498,7 +2514,7 @@ | ... | @@ -2498,7 +2514,7 @@ |
2498 | wsTrace('rx', msg); | 2514 | wsTrace('rx', msg); |
2499 | } | 2515 | } |
2500 | function wsTrace(rxtx, msg) { | 2516 | function wsTrace(rxtx, msg) { |
2501 | - console.log('[' + rxtx + '] ' + msg); | 2517 | + // console.log('[' + rxtx + '] ' + msg); |
2502 | } | 2518 | } |
2503 | 2519 | ||
2504 | // NOTE: Temporary hardcoded example for showing detail pane | 2520 | // NOTE: Temporary hardcoded example for showing detail pane |
... | @@ -2620,7 +2636,6 @@ | ... | @@ -2620,7 +2636,6 @@ |
2620 | emptySelect(); | 2636 | emptySelect(); |
2621 | } else if (nSel === 1) { | 2637 | } else if (nSel === 1) { |
2622 | singleSelect(); | 2638 | singleSelect(); |
2623 | - requestTrafficForMode(); | ||
2624 | } else { | 2639 | } else { |
2625 | multiSelect(); | 2640 | multiSelect(); |
2626 | } | 2641 | } |
... | @@ -2635,12 +2650,14 @@ | ... | @@ -2635,12 +2650,14 @@ |
2635 | function singleSelect() { | 2650 | function singleSelect() { |
2636 | // NOTE: detail is shown from showDetails event callback | 2651 | // NOTE: detail is shown from showDetails event callback |
2637 | requestDetails(); | 2652 | requestDetails(); |
2653 | + cancelTraffic(); | ||
2638 | requestTrafficForMode(); | 2654 | requestTrafficForMode(); |
2639 | } | 2655 | } |
2640 | 2656 | ||
2641 | function multiSelect() { | 2657 | function multiSelect() { |
2642 | haveDetails = true; | 2658 | haveDetails = true; |
2643 | populateMultiSelect(); | 2659 | populateMultiSelect(); |
2660 | + cancelTraffic(); | ||
2644 | requestTrafficForMode(); | 2661 | requestTrafficForMode(); |
2645 | } | 2662 | } |
2646 | 2663 | ||
... | @@ -2738,7 +2755,7 @@ | ... | @@ -2738,7 +2755,7 @@ |
2738 | function addSingleSelectActions(data) { | 2755 | function addSingleSelectActions(data) { |
2739 | detailPane.append('hr'); | 2756 | detailPane.append('hr'); |
2740 | // always want to allow 'show traffic' | 2757 | // always want to allow 'show traffic' |
2741 | - addAction(detailPane, 'Show Related Traffic', showTrafficAction); | 2758 | + addAction(detailPane, 'Show Related Traffic', showRelatedIntentsAction); |
2742 | 2759 | ||
2743 | if (data.type === 'switch') { | 2760 | if (data.type === 'switch') { |
2744 | addAction(detailPane, 'Show Device Flows', showDeviceLinkFlowsAction); | 2761 | addAction(detailPane, 'Show Device Flows', showDeviceLinkFlowsAction); |
... | @@ -2748,7 +2765,7 @@ | ... | @@ -2748,7 +2765,7 @@ |
2748 | function addMultiSelectActions() { | 2765 | function addMultiSelectActions() { |
2749 | detailPane.append('hr'); | 2766 | detailPane.append('hr'); |
2750 | // always want to allow 'show traffic' | 2767 | // always want to allow 'show traffic' |
2751 | - addAction(detailPane, 'Show Related Traffic', showTrafficAction); | 2768 | + addAction(detailPane, 'Show Related Traffic', showRelatedIntentsAction); |
2752 | // if exactly two hosts are selected, also want 'add host intent' | 2769 | // if exactly two hosts are selected, also want 'add host intent' |
2753 | if (nSel() === 2 && allSelectionsClass('host')) { | 2770 | if (nSel() === 2 && allSelectionsClass('host')) { |
2754 | addAction(detailPane, 'Create Host-to-Host Flow', addHostIntentAction); | 2771 | addAction(detailPane, 'Create Host-to-Host Flow', addHostIntentAction); | ... | ... |
-
Please register or login to post a comment