mirror of
https://github.com/opennetworkinglab/onos.git
synced 2026-05-05 20:26:16 +02:00
Added mechanism for apps to easily add their own custom link/node/host highlighting wihout having to create a new UI extensions
Change-Id: Iefa21d76190c60db79a4b07a8b22e301d29fe58e
This commit is contained in:
parent
d37181d7d1
commit
2b4de873e4
@ -55,6 +55,22 @@ public interface UiExtensionService {
|
||||
default void unregister(UiGlyphFactory factory) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the specified topo hilighter factory.
|
||||
*
|
||||
* @param factory UI topo higlighter factory to register
|
||||
*/
|
||||
default void register(UiTopoHighlighterFactory factory) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the specified user interface extension.
|
||||
*
|
||||
* @param factory UI topo higlighter factory to unregister
|
||||
*/
|
||||
default void unregister(UiTopoHighlighterFactory factory) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of registered user interface extensions.
|
||||
*
|
||||
@ -79,6 +95,15 @@ public interface UiExtensionService {
|
||||
return new ArrayList<UiGlyph>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of registered topo highlighter factories.
|
||||
*
|
||||
* @return list of highlighter factories
|
||||
*/
|
||||
default List<UiTopoHighlighterFactory> getTopoHighlighterFactories() {
|
||||
return new ArrayList<UiTopoHighlighterFactory>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the navigation pane localization bundle.
|
||||
*
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2021-present Open Networking Foundation
|
||||
*
|
||||
* 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;
|
||||
|
||||
import org.onosproject.ui.topo.Highlights;
|
||||
|
||||
/**
|
||||
* Abstraction of an entity capable of generating a set of topology highlights
|
||||
* for devices, hosts and links.
|
||||
*/
|
||||
public interface UiTopoHighlighter {
|
||||
|
||||
/**
|
||||
* Returns the self-assigned name of the hilighter.
|
||||
*
|
||||
* @return highlighter name
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Generate a set of highlights.
|
||||
*
|
||||
* @return topology highlights
|
||||
*/
|
||||
Highlights createHighlights();
|
||||
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2021-present Open Networking Foundation
|
||||
*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Abstraction of an entity capable of producing a a new topology highlighter.
|
||||
*/
|
||||
public interface UiTopoHighlighterFactory {
|
||||
|
||||
/**
|
||||
* Produces a new topology highlighter.
|
||||
*
|
||||
* @return newly created topology highlighter
|
||||
*/
|
||||
UiTopoHighlighter newTopoHighlighter();
|
||||
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2021-present Open Networking Foundation
|
||||
*
|
||||
* 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.onosproject.net.EdgeLink;
|
||||
import org.onosproject.net.Host;
|
||||
import org.onosproject.net.Link;
|
||||
import org.onosproject.net.host.HostService;
|
||||
import org.onosproject.net.link.LinkService;
|
||||
import org.onosproject.ui.UiExtensionService;
|
||||
import org.onosproject.ui.UiTopoHighlighter;
|
||||
import org.onosproject.ui.UiTopoHighlighterFactory;
|
||||
import org.onosproject.ui.topo.BaseLink;
|
||||
import org.onosproject.ui.topo.BaseLinkMap;
|
||||
import org.onosproject.ui.topo.Highlights;
|
||||
import org.onosproject.ui.topo.HostHighlight;
|
||||
import org.onosproject.ui.topo.LinkHighlight;
|
||||
import org.onosproject.ui.topo.Mod;
|
||||
import org.onosproject.ui.topo.NodeBadge;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
|
||||
import static org.onosproject.net.DefaultEdgeLink.createEdgeLinks;
|
||||
import static org.onosproject.ui.topo.NodeBadge.text;
|
||||
|
||||
@Component(immediate = false)
|
||||
public class SampleHighlighterFactory {
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MANDATORY)
|
||||
protected UiExtensionService uiExtensionService;
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MANDATORY)
|
||||
protected LinkService linkService;
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MANDATORY)
|
||||
protected HostService hostService;
|
||||
|
||||
private UiTopoHighlighterFactory foo = () ->
|
||||
new TestHighlighter("foo", new Mod("style=\"stroke: #0ff; stroke-width: 10px;\""));
|
||||
private UiTopoHighlighterFactory bar = () ->
|
||||
new TestHighlighter("bar", new Mod("style=\"stroke: #f0f; stroke-width: 4px; stroke-dasharray: 5 2;\""));
|
||||
|
||||
@Activate
|
||||
protected void activate() {
|
||||
uiExtensionService.register(foo);
|
||||
uiExtensionService.register(bar);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
uiExtensionService.unregister(foo);
|
||||
uiExtensionService.unregister(bar);
|
||||
}
|
||||
|
||||
private final class TestHighlighter implements UiTopoHighlighter {
|
||||
|
||||
private final String name;
|
||||
private final Mod mod;
|
||||
|
||||
private TestHighlighter(String name, Mod mod) {
|
||||
this.name = name;
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Highlights createHighlights() {
|
||||
Highlights highlights = new Highlights();
|
||||
BaseLinkMap linkMap = new BaseLinkMap();
|
||||
|
||||
// Create a map of base bi-links from the set of active links first.
|
||||
for (Link link : linkService.getActiveLinks()) {
|
||||
linkMap.add(link);
|
||||
}
|
||||
|
||||
for (Host host : hostService.getHosts()) {
|
||||
for (EdgeLink link : createEdgeLinks(host, false)) {
|
||||
linkMap.add(link);
|
||||
}
|
||||
|
||||
// Also add a host badge for kicks.
|
||||
HostHighlight hostHighlight = new HostHighlight(host.id().toString());
|
||||
hostHighlight.setBadge(text(NodeBadge.Status.WARN, name));
|
||||
highlights.add(hostHighlight);
|
||||
}
|
||||
|
||||
// Now scan through the links and annotate them with desired highlights
|
||||
for (BaseLink link : linkMap.biLinks()) {
|
||||
highlights.add(new LinkHighlight(link.linkId(), LinkHighlight.Flavor.PRIMARY_HIGHLIGHT)
|
||||
.addMod(mod).setLabel(name + "-" + link.one().src().port()));
|
||||
}
|
||||
|
||||
return highlights;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -113,6 +113,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
|
||||
private static final String REQ_SEL_INTENT_TRAFFIC = "requestSelectedIntentTraffic";
|
||||
private static final String SEL_INTENT = "selectIntent";
|
||||
private static final String REQ_ALL_TRAFFIC = "requestAllTraffic";
|
||||
private static final String REQ_CUSTOM_TRAFFIC = "requestCustomTraffic";
|
||||
private static final String REQ_DEV_LINK_FLOWS = "requestDeviceLinkFlows";
|
||||
private static final String CANCEL_TRAFFIC = "cancelTraffic";
|
||||
private static final String REQ_SUMMARY = "requestSummary";
|
||||
@ -246,6 +247,7 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
|
||||
new RemoveIntents(),
|
||||
|
||||
new ReqAllTraffic(),
|
||||
new ReqCustomTraffic(),
|
||||
new ReqDevLinkFlows(),
|
||||
new ReqRelatedIntents(),
|
||||
new ReqNextIntent(),
|
||||
@ -623,6 +625,17 @@ public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReqCustomTraffic extends RequestHandler {
|
||||
private ReqCustomTraffic() {
|
||||
super(REQ_CUSTOM_TRAFFIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(ObjectNode payload) {
|
||||
traffic.monitor((int) number(payload, "index"));
|
||||
}
|
||||
}
|
||||
|
||||
private NodeSelection makeNodeSelection(ObjectNode payload) {
|
||||
return new NodeSelection(payload, services.device(), services.host(),
|
||||
services.link());
|
||||
|
||||
@ -150,6 +150,14 @@ public class TrafficMonitor extends TrafficMonitorBase {
|
||||
msgHandler.sendHighlights(intentTraffic());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendCustomTraffic() {
|
||||
log.debug("sendCustomTraffic");
|
||||
if (topoHighlighter != null) {
|
||||
msgHandler.sendHighlights(topoHighlighter.createHighlights());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendClearHighlights() {
|
||||
log.debug("sendClearHighlights");
|
||||
|
||||
@ -38,6 +38,9 @@ import org.onosproject.net.DefaultEdgeLink;
|
||||
import org.onosproject.net.DeviceId;
|
||||
import org.onosproject.net.Link;
|
||||
import org.onosproject.net.statistic.Load;
|
||||
import org.onosproject.ui.UiExtensionService;
|
||||
import org.onosproject.ui.UiTopoHighlighter;
|
||||
import org.onosproject.ui.UiTopoHighlighterFactory;
|
||||
import org.onosproject.ui.impl.topo.TopoologyTrafficMessageHandlerAbstract;
|
||||
import org.onosproject.ui.impl.topo.util.IntentSelection;
|
||||
import org.onosproject.ui.impl.topo.util.ServicesBundle;
|
||||
@ -69,8 +72,7 @@ import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
|
||||
import static org.onosproject.net.statistic.PortStatisticsService.MetricType.BYTES;
|
||||
import static org.onosproject.net.statistic.PortStatisticsService.MetricType.PACKETS;
|
||||
import static org.onosproject.net.DefaultEdgeLink.createEdgeLinks;
|
||||
import static org.onosproject.ui.impl.TrafficMonitorBase.Mode.IDLE;
|
||||
import static org.onosproject.ui.impl.TrafficMonitorBase.Mode.SELECTED_INTENT;
|
||||
import static org.onosproject.ui.impl.TrafficMonitorBase.Mode.*;
|
||||
|
||||
/**
|
||||
* Base superclass for traffic monitor (both 'classic' and 'topo2' versions).
|
||||
@ -85,6 +87,7 @@ public abstract class TrafficMonitorBase extends AbstractTopoMonitor {
|
||||
protected IntentSelection selectedIntents = null;
|
||||
protected final TopoologyTrafficMessageHandlerAbstract msgHandler;
|
||||
protected NodeSelection selectedNodes = null;
|
||||
protected UiTopoHighlighter topoHighlighter = null;
|
||||
|
||||
protected void sendSelectedIntents() {
|
||||
log.debug("sendSelectedIntents: {}", selectedIntents);
|
||||
@ -285,7 +288,8 @@ public abstract class TrafficMonitorBase extends AbstractTopoMonitor {
|
||||
ALL_PORT_TRAFFIC_PKT_PS,
|
||||
DEV_LINK_FLOWS,
|
||||
RELATED_INTENTS,
|
||||
SELECTED_INTENT
|
||||
SELECTED_INTENT,
|
||||
CUSTOM_TRAFFIC_MONITOR
|
||||
}
|
||||
|
||||
/**
|
||||
@ -375,6 +379,22 @@ public abstract class TrafficMonitorBase extends AbstractTopoMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public synchronized void monitor(int index) {
|
||||
mode = CUSTOM_TRAFFIC_MONITOR;
|
||||
List<UiTopoHighlighterFactory> factories = services.get(UiExtensionService.class)
|
||||
.getTopoHighlighterFactories();
|
||||
if (factories.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UiTopoHighlighterFactory factory = factories.get(index % factories.size());
|
||||
topoHighlighter = factory.newTopoHighlighter();
|
||||
clearSelection();
|
||||
scheduleTask();
|
||||
sendCustomTraffic();
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor for traffic data to be sent back to the web client, under
|
||||
* the given mode, using the given selection of devices and hosts.
|
||||
@ -473,6 +493,12 @@ public abstract class TrafficMonitorBase extends AbstractTopoMonitor {
|
||||
*/
|
||||
protected abstract void sendSelectedIntentTraffic();
|
||||
|
||||
/**
|
||||
* Subclass should compile and send appropriate highlights data showing
|
||||
* custom traffic on links.
|
||||
*/
|
||||
protected abstract void sendCustomTraffic();
|
||||
|
||||
/**
|
||||
* Subclass should send a "clear highlights" event.
|
||||
*/
|
||||
@ -712,6 +738,9 @@ public abstract class TrafficMonitorBase extends AbstractTopoMonitor {
|
||||
case SELECTED_INTENT:
|
||||
sendSelectedIntentTraffic();
|
||||
break;
|
||||
case CUSTOM_TRAFFIC_MONITOR:
|
||||
sendCustomTraffic();
|
||||
break;
|
||||
|
||||
default:
|
||||
// RELATED_INTENTS and IDLE modes should never invoke
|
||||
|
||||
@ -49,6 +49,7 @@ import org.onosproject.ui.UiPreferencesService;
|
||||
import org.onosproject.ui.UiSessionToken;
|
||||
import org.onosproject.ui.UiTokenService;
|
||||
import org.onosproject.ui.UiTopo2OverlayFactory;
|
||||
import org.onosproject.ui.UiTopoHighlighterFactory;
|
||||
import org.onosproject.ui.UiTopoMap;
|
||||
import org.onosproject.ui.UiTopoMapFactory;
|
||||
import org.onosproject.ui.UiTopoOverlayFactory;
|
||||
@ -140,6 +141,7 @@ public class UiExtensionManager
|
||||
private final List<UiExtension> extensions = Lists.newArrayList();
|
||||
|
||||
private final List<UiGlyph> glyphs = Lists.newArrayList();
|
||||
private final List<UiTopoHighlighterFactory> highlighterFactories = Lists.newArrayList();
|
||||
|
||||
// Map of views to extensions
|
||||
private final Map<String, UiExtension> views = Maps.newHashMap();
|
||||
@ -365,6 +367,22 @@ public class UiExtensionManager
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void register(UiTopoHighlighterFactory factory) {
|
||||
checkPermission(UI_WRITE);
|
||||
if (!highlighterFactories.contains(factory)) {
|
||||
highlighterFactories.add(factory);
|
||||
UiWebSocketServlet.sendToAll(GUI_ADDED, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unregister(UiTopoHighlighterFactory factory) {
|
||||
checkPermission(UI_WRITE);
|
||||
highlighterFactories.remove(factory);
|
||||
UiWebSocketServlet.sendToAll(GUI_REMOVED, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<UiExtension> getExtensions() {
|
||||
checkPermission(UI_READ);
|
||||
@ -377,6 +395,12 @@ public class UiExtensionManager
|
||||
return ImmutableList.copyOf(glyphs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<UiTopoHighlighterFactory> getTopoHighlighterFactories() {
|
||||
checkPermission(UI_READ);
|
||||
return ImmutableList.copyOf(highlighterFactories);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized UiExtension getViewExtension(String viewId) {
|
||||
checkPermission(UI_READ);
|
||||
|
||||
@ -64,6 +64,10 @@ public class Traffic2Monitor extends TrafficMonitorBase {
|
||||
msgHandler.sendHighlights(trafficSummary(TrafficLink.StatsType.FLOW_STATS));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendCustomTraffic() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendAllPortTrafficBits() {
|
||||
log.debug("TOPO-2-TRAFFIC: sendAllPortTrafficBits");
|
||||
|
||||
@ -37,6 +37,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
*/
|
||||
public class ServicesBundle {
|
||||
|
||||
private ServiceDirectory directory;
|
||||
|
||||
private ClusterService clusterService;
|
||||
|
||||
private TopologyService topologyService;
|
||||
@ -60,6 +62,7 @@ public class ServicesBundle {
|
||||
*/
|
||||
public ServicesBundle(ServiceDirectory directory) {
|
||||
checkNotNull(directory, "Directory cannot be null");
|
||||
this.directory = directory;
|
||||
|
||||
clusterService = directory.get(ClusterService.class);
|
||||
|
||||
@ -184,4 +187,15 @@ public class ServicesBundle {
|
||||
public PortStatisticsService portStats() {
|
||||
return portStatsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the implementation of the specified service class.
|
||||
*
|
||||
* @param serviceClass service class
|
||||
* @param <T> class of service
|
||||
* @return implementation of the service class
|
||||
*/
|
||||
public <T> T get(Class<T> serviceClass) {
|
||||
return directory.get(serviceClass);
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,6 +103,7 @@ tr_btn_show_device_flows=Show Device Flows
|
||||
tr_btn_show_related_traffic=Show Related Traffic
|
||||
tr_btn_cancel_monitoring=Cancel traffic monitoring
|
||||
tr_btn_monitor_all=Monitor all traffic
|
||||
tr_btn_monitor_custom_all=Custom traffic monitor
|
||||
tr_btn_show_dev_link_flows=Show device link flows
|
||||
tr_btn_show_all_rel_intents=Show all related intents
|
||||
tr_btn_show_prev_rel_intent=Show previous related intent
|
||||
|
||||
@ -1023,6 +1023,7 @@
|
||||
|
||||
function clearLinkTrafficStyle() {
|
||||
link.style('stroke-width', null)
|
||||
.style('stroke', null)
|
||||
.classed(allTrafficClasses, false);
|
||||
}
|
||||
|
||||
|
||||
@ -398,6 +398,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
const stylePattern = /style=\"[^\"]*\"/g;
|
||||
|
||||
data.links.forEach(function (link) {
|
||||
var ldata = api.findLinkById(link.id);
|
||||
|
||||
@ -405,6 +407,14 @@
|
||||
if (!link.subdue) {
|
||||
api.unsupLink(ldata.key, less);
|
||||
}
|
||||
var styleFound = link.css.match(stylePattern);
|
||||
if (styleFound) {
|
||||
link.css = link.css.replace(stylePattern, '');
|
||||
var style = styleFound[0].replace('style="', '').replace('"$', '')
|
||||
ldata.el.attr('style', style);
|
||||
} else {
|
||||
ldata.el.attr('style', '');
|
||||
}
|
||||
ldata.el.classed(link.css, true);
|
||||
ldata.label = link.label;
|
||||
|
||||
|
||||
@ -47,7 +47,8 @@
|
||||
// internal state
|
||||
var trafficMode = null,
|
||||
hoverMode = null,
|
||||
allTrafficIndex = 0;
|
||||
allTrafficIndex = 0,
|
||||
customTrafficIndex = 0;
|
||||
|
||||
|
||||
// === -----------------------------------------------------
|
||||
@ -137,6 +138,16 @@
|
||||
allTrafficIndex = (allTrafficIndex + 1) % 3;
|
||||
}
|
||||
|
||||
function showCustomTraffic() {
|
||||
trafficMode = 'allCustom';
|
||||
hoverMode = null;
|
||||
wss.sendEvent('requestCustomTraffic', {
|
||||
index: customTrafficIndex,
|
||||
});
|
||||
flash.flash('Custom Traffic');
|
||||
customTrafficIndex = customTrafficIndex + 1;
|
||||
}
|
||||
|
||||
function showDeviceLinkFlows() {
|
||||
trafficMode = hoverMode = 'flows';
|
||||
requestDeviceLinkFlows();
|
||||
@ -270,6 +281,7 @@
|
||||
showNextIntent: showNextIntent,
|
||||
showSelectedIntentTraffic: showSelectedIntentTraffic,
|
||||
selectIntent: selectIntent,
|
||||
showCustomTraffic: showCustomTraffic,
|
||||
|
||||
// invoked from mouseover/mouseout and selection change
|
||||
requestTrafficForMode: requestTrafficForMode,
|
||||
|
||||
@ -109,9 +109,14 @@
|
||||
tt: function () { return topoLion('tr_btn_monitor_sel_intent'); },
|
||||
gid: 'm_intentTraffic',
|
||||
},
|
||||
C: {
|
||||
cb: function () { tts.showCustomTraffic(); },
|
||||
tt: function () { return topoLion('tr_btn_monitor_custom_all'); },
|
||||
gid: 'm_allTraffic',
|
||||
},
|
||||
|
||||
_keyOrder: [
|
||||
'0', 'A', 'F', 'V', 'leftArrow', 'rightArrow', 'W',
|
||||
'0', 'A', 'F', 'V', 'leftArrow', 'rightArrow', 'W', 'C'
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user