Adding server-side user preferences.
More work still needs to get done to allow client to process
server-pushed preferences updates.
Change-Id: I6e80e3f3677285cb19cfa3b6240c1b13aac56622
diff --git a/core/api/src/main/java/org/onosproject/ui/UiConnection.java b/core/api/src/main/java/org/onosproject/ui/UiConnection.java
index ead7b0d..4ffc74a 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiConnection.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiConnection.java
@@ -23,6 +23,13 @@
public interface UiConnection {
/**
+ * Returns the name of the logged-in user for which this connection exists.
+ *
+ * @return logged in user name
+ */
+ String userName();
+
+ /**
* Sends the specified JSON message to the user interface client.
*
* @param message message to send
diff --git a/core/api/src/main/java/org/onosproject/ui/UiPreferencesService.java b/core/api/src/main/java/org/onosproject/ui/UiPreferencesService.java
new file mode 100644
index 0000000..3cc046f
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/UiPreferencesService.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 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;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Service for tracking user interface preferences.
+ */
+public interface UiPreferencesService {
+
+ /**
+ * Returns the list of user names that have user preferences available.
+ *
+ * @return list of user names
+ */
+ Set<String> getUserNames();
+
+ /**
+ * Returns an immutable copy of the preferences for the specified user.
+ *
+ * @param userName user name
+ * @return map of user preferences
+ */
+ Map<String, ObjectNode> getPreferences(String userName);
+
+ /**
+ * Sets the named preference for the specified user.
+ *
+ * @param userName user name
+ * @param preference name of the user preference
+ * @param value preference value
+ */
+ void setPreference(String userName, String preference, ObjectNode value);
+
+}
diff --git a/web/gui/pom.xml b/web/gui/pom.xml
index efa8520..17a7742 100644
--- a/web/gui/pom.xml
+++ b/web/gui/pom.xml
@@ -54,6 +54,11 @@
</dependency>
<dependency>
<groupId>org.onosproject</groupId>
+ <artifactId>onos-core-serializers</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.onosproject</groupId>
<artifactId>onos-incubator-api</artifactId>
</dependency>
<dependency>
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
index ac1a8bb..dd345b4 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
@@ -15,17 +15,22 @@
*/
package org.onosproject.ui.impl;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import org.onlab.osgi.ServiceNotFoundException;
import org.onosproject.rest.AbstractInjectionResource;
import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiPreferencesService;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
@@ -42,12 +47,21 @@
private static final String INDEX = "index.html";
private static final String NOT_READY = "not-ready.html";
+ private static final String INJECT_USER_START = "<!-- {INJECTED-USER-START} -->";
+ private static final String INJECT_USER_END = "<!-- {INJECTED-USER-END} -->";
+
private static final String INJECT_CSS_START = "<!-- {INJECTED-STYLESHEETS-START} -->";
private static final String INJECT_CSS_END = "<!-- {INJECTED-STYLESHEETS-END} -->";
private static final String INJECT_JS_START = "<!-- {INJECTED-JAVASCRIPT-START} -->";
private static final String INJECT_JS_END = "<!-- {INJECTED-JAVASCRIPT-END} -->";
+ private static final byte[] SCRIPT_START = "\n<script>\n".getBytes();
+ private static final byte[] SCRIPT_END = "\n</script>\n\n".getBytes();
+
+ @Context
+ private SecurityContext ctx;
+
@GET
@Produces(MediaType.TEXT_HTML)
public Response getMainIndex() throws IOException {
@@ -62,14 +76,25 @@
InputStream indexTemplate = classLoader.getResourceAsStream(INDEX);
String index = new String(toByteArray(indexTemplate));
- int p1s = split(index, 0, INJECT_JS_START) - INJECT_JS_START.length();
+ int p0s = split(index, 0, INJECT_USER_START) - INJECT_USER_START.length();
+ int p0e = split(index, p0s, INJECT_USER_END);
+ int p1s = split(index, p0e, INJECT_JS_START) - INJECT_JS_START.length();
int p1e = split(index, p1s, INJECT_JS_END);
int p2s = split(index, p1e, INJECT_CSS_START) - INJECT_CSS_START.length();
int p2e = split(index, p2s, INJECT_CSS_END);
int p3s = split(index, p2e, null);
+ // FIXME: use global opaque auth token to allow secure failover
+ String userName = ctx.getUserPrincipal().getName();
+ String auth = "var onosAuth='" + userName + "';\n";
+
StreamEnumeration streams =
- new StreamEnumeration(of(stream(index, 0, p1s),
+ new StreamEnumeration(of(stream(index, 0, p0s),
+ new ByteArrayInputStream(SCRIPT_START),
+ stream(auth, 0, auth.length()),
+ userPreferences(userName),
+ new ByteArrayInputStream(SCRIPT_END),
+ stream(index, p0e, p1s),
includeJs(service),
stream(index, p1e, p2s),
includeCss(service),
@@ -78,6 +103,15 @@
return Response.ok(new SequenceInputStream(streams)).build();
}
+ // Produces an input stream including user preferences.
+ private InputStream userPreferences(String userName) {
+ UiPreferencesService service = get(UiPreferencesService.class);
+ ObjectNode prefs = mapper().createObjectNode();
+ service.getPreferences(userName).forEach(prefs::set);
+ String string = "var userPrefs = " + prefs.toString() + ";";
+ return new ByteArrayInputStream(string.getBytes());
+ }
+
// Produces an input stream including JS injections from all extensions.
private InputStream includeJs(UiExtensionService service) {
Builder<InputStream> builder = ImmutableList.builder();
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
index 3bf6c27..55ff611 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -16,7 +16,19 @@
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.BooleanNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.ShortNode;
+import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -26,40 +38,49 @@
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.KryoNamespace;
import org.onosproject.mastership.MastershipService;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapEvent;
+import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
import org.onosproject.ui.UiExtension;
import org.onosproject.ui.UiExtensionService;
import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiPreferencesService;
import org.onosproject.ui.UiTopoOverlayFactory;
import org.onosproject.ui.UiView;
import org.onosproject.ui.UiViewHidden;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.collect.ImmutableList.of;
import static java.util.stream.Collectors.toSet;
-import static org.onosproject.ui.UiView.Category.NETWORK;
-import static org.onosproject.ui.UiView.Category.PLATFORM;
-
import static org.onosproject.security.AppGuard.checkPermission;
import static org.onosproject.security.AppPermission.Type.UI_READ;
import static org.onosproject.security.AppPermission.Type.UI_WRITE;
+import static org.onosproject.ui.UiView.Category.NETWORK;
+import static org.onosproject.ui.UiView.Category.PLATFORM;
/**
* Manages the user interface extensions.
*/
@Component(immediate = true)
@Service
-public class UiExtensionManager implements UiExtensionService, SpriteService {
+public class UiExtensionManager implements UiExtensionService, UiPreferencesService, SpriteService {
private static final ClassLoader CL = UiExtensionManager.class.getClassLoader();
private static final String CORE = "core";
private static final String GUI_ADDED = "guiAdded";
private static final String GUI_REMOVED = "guiRemoved";
+ private static final String UPDATE_PREFS = "updatePrefs";
private final Logger log = LoggerFactory.getLogger(getClass());
@@ -72,10 +93,19 @@
// Core views & core extension
private final UiExtension core = createCoreExtension();
-
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected MastershipService mastershipService;
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ protected StorageService storageService;
+
+ // User preferences
+ private EventuallyConsistentMap<String, ObjectNode> prefs;
+ private final EventuallyConsistentMapListener<String, ObjectNode> prefsListener =
+ new InternalPrefsListener();
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
// Creates core UI extension
private UiExtension createCoreExtension() {
List<UiView> coreViews = of(
@@ -98,6 +128,7 @@
UiMessageHandlerFactory messageHandlerFactory =
() -> ImmutableList.of(
+ new UserPreferencesMessageHandler(),
new TopologyViewMessageHandler(),
new DeviceViewMessageHandler(),
new LinkViewMessageHandler(),
@@ -128,12 +159,28 @@
@Activate
public void activate() {
+ KryoNamespace.Builder kryoBuilder = new KryoNamespace.Builder()
+ .register(KryoNamespaces.API)
+ .register(ObjectNode.class, ArrayNode.class,
+ JsonNodeFactory.class, LinkedHashMap.class,
+ TextNode.class, BooleanNode.class,
+ LongNode.class, DoubleNode.class, ShortNode.class,
+ IntNode.class, NullNode.class);
+
+ prefs = storageService.<String, ObjectNode>eventuallyConsistentMapBuilder()
+ .withName("onos-user-preferences")
+ .withSerializer(kryoBuilder)
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .withPersistence()
+ .build();
+ prefs.addListener(prefsListener);
register(core);
log.info("Started");
}
@Deactivate
public void deactivate() {
+ prefs.removeListener(prefsListener);
UiWebSocketServlet.closeAll();
unregister(core);
log.info("Stopped");
@@ -171,6 +218,27 @@
return views.get(viewId);
}
+ @Override
+ public Set<String> getUserNames() {
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ prefs.keySet().forEach(k -> builder.add(userName(k)));
+ return builder.build();
+ }
+
+ @Override
+ public Map<String, ObjectNode> getPreferences(String userName) {
+ ImmutableMap.Builder<String, ObjectNode> builder = ImmutableMap.builder();
+ prefs.entrySet().stream()
+ .filter(e -> e.getKey().startsWith(userName + "/"))
+ .forEach(e -> builder.put(keyName(e.getKey()), e.getValue()));
+ return builder.build();
+ }
+
+ @Override
+ public void setPreference(String userName, String preference, ObjectNode value) {
+ prefs.put(key(userName, preference), value);
+ }
+
// =====================================================================
// Provisional tracking of sprite definitions
@@ -192,4 +260,33 @@
return sprites.get(name);
}
+ private String key(String userName, String keyName) {
+ return userName + "/" + keyName;
+ }
+
+ private String userName(String key) {
+ return key.split("/")[0];
+ }
+
+ private String keyName(String key) {
+ return key.split("/")[1];
+ }
+
+ // Auxiliary listener to preference map events.
+ private class InternalPrefsListener
+ implements EventuallyConsistentMapListener<String, ObjectNode> {
+ @Override
+ public void event(EventuallyConsistentMapEvent<String, ObjectNode> event) {
+ String userName = userName(event.key());
+ if (event.type() == EventuallyConsistentMapEvent.Type.PUT) {
+ UiWebSocketServlet.sendToUser(userName, UPDATE_PREFS, jsonPrefs());
+ }
+ }
+
+ private ObjectNode jsonPrefs() {
+ ObjectNode json = mapper.createObjectNode();
+ prefs.entrySet().forEach(e -> json.set(keyName(e.getKey()), e.getValue()));
+ return json;
+ }
+ }
}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
index d2d6705..efe30843 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
@@ -54,6 +54,7 @@
private Connection connection;
private FrameConnection control;
+ private String userName;
private final ObjectMapper mapper = new ObjectMapper();
@@ -66,9 +67,16 @@
* Creates a new web-socket for serving data to GUI.
*
* @param directory service directory
+ * @param userName user name of the logged-in user
*/
- public UiWebSocket(ServiceDirectory directory) {
+ public UiWebSocket(ServiceDirectory directory, String userName) {
this.directory = directory;
+ this.userName = userName;
+ }
+
+ @Override
+ public String userName() {
+ return userName;
}
/**
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
index 564b07f..a9f349f 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
@@ -70,7 +70,10 @@
if (isStopped) {
return null;
}
- UiWebSocket socket = new UiWebSocket(directory);
+
+ // FIXME: Replace this with globally shared opaque token to allow secure failover
+ String userName = request.getUserPrincipal().getName();
+ UiWebSocket socket = new UiWebSocket(directory, userName);
synchronized (sockets) {
sockets.add(socket);
}
@@ -89,6 +92,20 @@
}
}
+ /**
+ * Sends the specified message to all the GUI clients of the specified user.
+ *
+ * @param userName user name
+ * @param type message type
+ * @param payload message payload
+ */
+ static void sendToUser(String userName, String type, ObjectNode payload) {
+ if (instance != null) {
+ instance.sockets.stream().filter(ws -> userName.equals(ws.userName()))
+ .forEach(ws -> ws.sendMessage(type, 0, payload));
+ }
+ }
+
// Task for pruning web-sockets that are idle.
private class Pruner extends TimerTask {
@Override
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UserPreferencesMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/UserPreferencesMessageHandler.java
new file mode 100644
index 0000000..cc6fa59
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UserPreferencesMessageHandler.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 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.onosproject.ui.RequestHandler;
+import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.UiPreferencesService;
+
+import java.util.Collection;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Message handler for intercepting user preferences messages.
+ */
+class UserPreferencesMessageHandler extends UiMessageHandler {
+
+ private static final String UPDATE_PREFS_REQ = "updatePrefReq";
+ private static final String KEY = "key";
+ private static final String VALUE = "value";
+
+ @Override
+ protected Collection<RequestHandler> createRequestHandlers() {
+ return ImmutableSet.of(new UpdatePreferencesRequest());
+ }
+
+ private final class UpdatePreferencesRequest extends RequestHandler {
+ private UpdatePreferencesRequest() {
+ super(UPDATE_PREFS_REQ);
+ }
+
+ @Override
+ public void process(long sid, ObjectNode payload) {
+ if (!isNullOrEmpty(connection().userName())) {
+ UiPreferencesService service = get(UiPreferencesService.class);
+ service.setPreference(connection().userName(),
+ payload.get(KEY).asText(),
+ (ObjectNode) payload.get(VALUE));
+ }
+ }
+ }
+}
diff --git a/web/gui/src/main/webapp/app/fw/layer/loading.js b/web/gui/src/main/webapp/app/fw/layer/loading.js
index 86dd56d..a9e141eb 100644
--- a/web/gui/src/main/webapp/app/fw/layer/loading.js
+++ b/web/gui/src/main/webapp/app/fw/layer/loading.js
@@ -122,9 +122,9 @@
angular.module('onosLayer')
.factory('LoadingService',
- ['$log', '$timeout', 'ThemeService', 'FnService',
+ ['$log', '$timeout', 'ThemeService', 'FnService', 'WebSocketService',
- function (_$log_, _$timeout_, _ts_, _fs_) {
+ function (_$log_, _$timeout_, _ts_, _fs_, wss) {
$log = _$log_;
$timeout = _$timeout_;
ts = _ts_;
@@ -132,11 +132,13 @@
preloadImages();
- return {
+ var self = {
start: start,
stop: stop,
waiting: waiting
};
+ wss._setLoadingDelegate(self);
+ return self;
}]);
}());
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/app/fw/layer/veil.js b/web/gui/src/main/webapp/app/fw/layer/veil.js
index fc0530a..fddfe7f 100644
--- a/web/gui/src/main/webapp/app/fw/layer/veil.js
+++ b/web/gui/src/main/webapp/app/fw/layer/veil.js
@@ -79,21 +79,23 @@
angular.module('onosLayer')
.factory('VeilService',
- ['$log', '$route', 'FnService', 'KeyService', 'GlyphService',
+ ['$log', '$route', 'FnService', 'KeyService', 'GlyphService', 'WebSocketService',
- function (_$log_, _$route_, _fs_, _ks_, _gs_) {
+ function (_$log_, _$route_, _fs_, _ks_, _gs_, wss) {
$log = _$log_;
$route = _$route_;
fs = _fs_;
ks = _ks_;
gs = _gs_;
- return {
+ var self = {
init: init,
show: show,
hide: hide,
lostServer: lostServer
};
+ wss._setVeilDelegate(self);
+ return self;
}]);
}());
diff --git a/web/gui/src/main/webapp/app/fw/remote/websocket.js b/web/gui/src/main/webapp/app/fw/remote/websocket.js
index e39a7f9..92d2d2e 100644
--- a/web/gui/src/main/webapp/app/fw/remote/websocket.js
+++ b/web/gui/src/main/webapp/app/fw/remote/websocket.js
@@ -59,7 +59,7 @@
function handleOpen() {
$log.info('Web socket open - ', url);
- vs.hide();
+ vs && vs.hide();
if (fs.debugOn('txrx')) {
$log.debug('Sending ' + pendingEvents.length + ' pending event(s)...');
@@ -105,14 +105,14 @@
var gsucc;
$log.info('Web socket closed');
- ls.stop();
+ ls && ls.stop();
wsUp = false;
if (gsucc = findGuiSuccessor()) {
createWebSocket(webSockOpts, gsucc);
} else {
// If no controllers left to contact, show the Veil...
- vs.show([
+ vs && vs.show([
'Oops!',
'Web-socket connection to server closed...',
'Try refreshing the page.'
@@ -296,22 +296,29 @@
}
}
+ // Binds the veil service as a delegate
+ function setVeilDelegate(vd) {
+ vs = vd;
+ }
+
+ // Binds the loading service as a delegate
+ function setLoadingDelegate(ld) {
+ ls = ld;
+ }
+
// ============================
// ===== Definition of module
angular.module('onosRemote')
.factory('WebSocketService',
['$log', '$location', 'FnService', 'UrlFnService', 'WSock',
- 'VeilService', 'LoadingService',
- function (_$log_, _$loc_, _fs_, _ufs_, _wsock_, _vs_, _ls_) {
+ function (_$log_, _$loc_, _fs_, _ufs_, _wsock_) {
$log = _$log_;
$loc = _$loc_;
fs = _fs_;
ufs = _ufs_;
wsock = _wsock_;
- vs = _vs_;
- ls = _ls_;
bindHandlers(builtinHandlers);
@@ -324,7 +331,10 @@
addOpenListener: addOpenListener,
removeOpenListener: removeOpenListener,
sendEvent: sendEvent,
- isConnected: function () { return wsUp; }
+ isConnected: function () { return wsUp; },
+
+ _setVeilDelegate: setVeilDelegate,
+ _setLoadingDelegate: setLoadingDelegate
};
}
]);
diff --git a/web/gui/src/main/webapp/app/fw/util/prefs.js b/web/gui/src/main/webapp/app/fw/util/prefs.js
index 02a2390..ad3dc04 100644
--- a/web/gui/src/main/webapp/app/fw/util/prefs.js
+++ b/web/gui/src/main/webapp/app/fw/util/prefs.js
@@ -21,54 +21,14 @@
'use strict';
// injected refs
- var $log, $cookies, fs;
+ var $log, fs, wss;
// internal state
var cache = {};
- // NOTE: in Angular 1.3.5, $cookies is just a simple object, and
- // cookie values are just strings. From the 1.3.5 docs:
- //
- // "Only a simple Object is exposed and by adding or removing
- // properties to/from this object, new cookies are created/deleted
- // at the end of current $eval. The object's properties can only
- // be strings."
- //
- // We may want to upgrade the version of Angular sometime soon
- // since later version support objects as cookie values.
-
- // NOTE: prefs represented as simple name/value pairs
- // => a temporary restriction while we are encoding into cookies
- /*
- {
- foo: 1,
- bar: 0,
- goo: 2
- }
-
- stored as "foo:1,bar:0,goo:2"
- */
-
- // reads cookie with given name and returns an object rep of its value
- // or null if no such cookie is set
- function getPrefs(name) {
- var cook = $cookies[name],
- bits,
- obj = {};
-
- if (cook) {
- bits = cook.split(',');
- bits.forEach(function (value) {
- var x = value.split(':');
- obj[x[0]] = x[1];
- });
-
- // update the cache
- cache[name] = obj;
- return obj;
- }
- // perhaps we have a cached copy..
- return cache[name];
+ // returns the preference by the specified name
+ function getPrefs(name, defaults) {
+ return cache[name] || defaults;
}
// converts string values to numbers for selected (or all) keys
@@ -89,34 +49,28 @@
}
function setPrefs(name, obj) {
- var bits = [],
- str;
-
- angular.forEach(obj, function (value, key) {
- bits.push(key + ':' + value);
- });
- str = bits.join(',');
-
- // keep a cached copy of the object
+ // keep a cached copy of the object and send an update to server
cache[name] = obj;
-
- // The angular way of doing this...
- // $cookies[name] = str;
- // ...but it appears that this gets delayed, and doesn't 'stick' ??
-
- // FORCE cookie to be set by writing directly to document.cookie...
- document.cookie = name + '=' + encodeURIComponent(str);
- if (fs.debugOn('prefs')) {
- $log.debug('<<>> Wrote cookie <'+name+'>:', str);
- }
+ wss.sendEvent('updatePrefReq', { key: name, value: obj });
+ }
+
+ function updatePrefs(data) {
+ $log.info('User properties updated');
+ cache[data.key] = data.value;
}
angular.module('onosUtil')
- .factory('PrefsService', ['$log', '$cookies', 'FnService',
- function (_$log_, _$cookies_, _fs_) {
+ .factory('PrefsService', ['$log', 'FnService', 'WebSocketService',
+ function (_$log_, _fs_, _wss_) {
$log = _$log_;
- $cookies = _$cookies_;
fs = _fs_;
+ wss = _wss_;
+
+ cache = userPrefs;
+
+ wss.bindHandlers({
+ updatePrefs: updatePrefs
+ });
return {
getPrefs: getPrefs,
diff --git a/web/gui/src/main/webapp/app/fw/util/theme.js b/web/gui/src/main/webapp/app/fw/util/theme.js
index 0e0ee64..4441402 100644
--- a/web/gui/src/main/webapp/app/fw/util/theme.js
+++ b/web/gui/src/main/webapp/app/fw/util/theme.js
@@ -20,7 +20,7 @@
(function () {
'use strict';
- var $log, fs;
+ var $log, fs, ps;
var themes = ['light', 'dark'],
themeStr = themes.join(' '),
@@ -29,7 +29,7 @@
nextListenerId = 1;
function init() {
- thidx = 0;
+ thidx = ps.getPrefs('theme', { idx: 0 }).idx;
updateBodyClass();
}
@@ -37,10 +37,11 @@
return themes[thidx];
}
- function setTheme(t) {
+ function setTheme(t, force) {
var idx = themes.indexOf(t);
- if (idx > -1 && idx !== thidx) {
+ if (force || idx > -1 && idx !== thidx) {
thidx = idx;
+ ps.setPrefs('theme', { idx: thidx });
updateBodyClass();
themeEvent('set');
}
@@ -49,6 +50,7 @@
function toggleTheme() {
var i = thidx + 1;
thidx = (i===themes.length) ? 0 : i;
+ ps.setPrefs('theme', { idx: thidx });
updateBodyClass();
themeEvent('toggle');
return getTheme();
@@ -97,11 +99,11 @@
}
angular.module('onosUtil')
- .factory('ThemeService', ['$log', 'FnService',
- function (_$log_, _fs_) {
+ .factory('ThemeService', ['$log', 'FnService', 'PrefsService',
+ function (_$log_, _fs_, _ps_) {
$log = _$log_;
fs = _fs_;
- thidx = 0;
+ ps = _ps_;
return {
init: init,
diff --git a/web/gui/src/main/webapp/app/fw/widget/toolbar.js b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
index 050afd0..3b9e5ed 100644
--- a/web/gui/src/main/webapp/app/fw/widget/toolbar.js
+++ b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
@@ -219,6 +219,10 @@
}
}
+ function isVisible() {
+ return panel.isVisible();
+ }
+
return {
addButton: addButton,
addToggle: addToggle,
@@ -228,7 +232,8 @@
show: show,
hide: hide,
- toggle: toggle
+ toggle: toggle,
+ isVisible: isVisible
};
}
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index 9b532f6..069fa42 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -30,7 +30,7 @@
// references to injected services
var $scope, $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps, th,
- tds, tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, tspr, ttip, tov;
+ tds, t3s, tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, tspr, ttip, tov;
// DOM elements
var ovtopo, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer;
@@ -369,16 +369,14 @@
function setUpMap($loc) {
var qp = $loc.search(),
- pr = ps.getPrefs('topo_mapid'),
- mi1 = qp.mapid,
- mi2 = pr && pr.id,
- mapId = mi1 || mi2 || 'usa',
- ms1 = qp.mapscale,
- ms2 = pr && pr.scale,
- mapScale = ms1 || ms2 || 1,
- t1 = qp.tint,
- t2 = pr && pr.tint,
- tint = t1 || t2 || 'off',
+ pr = ps.getPrefs('topo_mapid', {
+ id: qp.mapid || 'usa',
+ scale: qp.mapscale || 1,
+ tint: qp.tint || 'off'
+ }),
+ mapId = pr.id,
+ mapScale = pr.scale,
+ tint = pr.tint,
promise,
cfilter;
@@ -441,8 +439,8 @@
function setUpSprites($loc, tspr) {
var s1 = $loc.search().sprites,
- s2 = ps.getPrefs('topo_sprites'),
- sprId = s1 || (s2 && s2.id);
+ s2 = ps.getPrefs('topo_sprites', { id: s1 }),
+ sprId = s2.id;
spriteG = zoomLayer.append ('g').attr('id', 'topo-sprites');
if (sprId) {
@@ -463,7 +461,7 @@
function restoreConfigFromPrefs() {
// NOTE: toolbar will have set this for us..
- prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+ prefsState = ps.asNumbers(ps.getPrefs('topo_prefs', ttbs.defaultPrefs));
$log.debug('TOPO- Prefs State:', prefsState);
@@ -476,6 +474,7 @@
togglePorts(prefsState.porthl);
toggleMap(prefsState.bg);
toggleSprites(prefsState.spr);
+ t3s.setDevLabIndex(prefsState.dlbls);
flash.enable(true);
}
@@ -484,7 +483,7 @@
// have opened the websocket to the server; hence this extra function
// invoked after tes.start()
function restoreSummaryFromPrefs() {
- prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+ prefsState = ps.asNumbers(ps.getPrefs('topo_prefs', ttbs.defaultPrefs));
$log.debug('TOPO- Prefs SUMMARY State:', prefsState.summary);
flash.enable(false);
@@ -506,7 +505,7 @@
'$cookies', 'FnService', 'MastService', 'KeyService', 'ZoomService',
'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
'WebSocketService', 'PrefsService', 'ThemeService',
- 'TopoDialogService',
+ 'TopoDialogService', 'TopoD3Service',
'TopoEventService', 'TopoForceService', 'TopoPanelService',
'TopoInstService', 'TopoSelectService', 'TopoLinkService',
'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService',
@@ -515,7 +514,7 @@
function (_$scope_, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_,
_zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _th_,
- _tds_, _tes_,
+ _tds_, _t3s_, _tes_,
_tfs_, _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_,
_ttbs_, _tspr_, _ttip_, _tov_) {
var params = $loc.search(),
@@ -545,6 +544,7 @@
ps = _ps_;
th = _th_;
tds = _tds_;
+ t3s = _t3s_;
tes = _tes_;
tfs = _tfs_;
// TODO: consider funnelling actions through TopoForceService...
@@ -601,7 +601,7 @@
setUpNoDevs();
setUpMap($loc).then(
function (proj) {
- var z = ps.getPrefs('topo_zoom') || {tx:0, ty:0, sc:1};
+ var z = ps.getPrefs('topo_zoom', { tx:0, ty:0, sc:1 });
zoomer.panZoom([z.tx, z.ty], z.sc);
$log.debug('** Zoom restored:', z);
diff --git a/web/gui/src/main/webapp/app/view/topo/topoD3.js b/web/gui/src/main/webapp/app/view/topo/topoD3.js
index a14ccbe..ef704c7 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -23,7 +23,7 @@
'use strict';
// injected refs
- var $log, fs, sus, is, ts;
+ var $log, fs, sus, is, ts, ps, ttbs;
// api to topoForce
var api;
@@ -159,7 +159,7 @@
// ====
function incDevLabIndex() {
- deviceLabelIndex = (deviceLabelIndex+1) % 3;
+ setDevLabIndex(deviceLabelIndex+1);
switch(deviceLabelIndex) {
case 0: return 'Hide device labels';
case 1: return 'Show friendly device labels';
@@ -167,6 +167,13 @@
}
}
+ function setDevLabIndex(mode) {
+ deviceLabelIndex = mode % 3;
+ var p = ps.getPrefs('topo_prefs', ttbs.defaultPrefs);
+ p.dlbls = deviceLabelIndex;
+ ps.setPrefs('topo_prefs', p);
+ }
+
// Returns the newly computed bounding box of the rectangle
function adjustRectToFitText(n) {
var text = n.select('text'),
@@ -599,13 +606,16 @@
angular.module('ovTopo')
.factory('TopoD3Service',
['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
+ 'PrefsService', 'TopoToolbarService',
- function (_$log_, _fs_, _sus_, _is_, _ts_) {
+ function (_$log_, _fs_, _sus_, _is_, _ts_, _ps_, _ttbs_) {
$log = _$log_;
fs = _fs_;
sus = _sus_;
is = _is_;
ts = _ts_;
+ ps = _ps_;
+ ttbs = _ttbs_;
icfg = is.iconConfig();
@@ -620,6 +630,7 @@
destroyD3: destroyD3,
incDevLabIndex: incDevLabIndex,
+ setDevLabIndex: setDevLabIndex,
adjustRectToFitText: adjustRectToFitText,
hostLabel: hostLabel,
deviceLabel: deviceLabel,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
index 34e85f3..e1cef91 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
@@ -529,6 +529,7 @@
showSummary: showSummary,
toggleSummary: toggleSummary,
+ hideSummary: hideSummaryPanel,
toggleUseDetailsFlag: toggleUseDetailsFlag,
displaySingle: displaySingle,
@@ -538,8 +539,6 @@
displaySomething: displaySomething,
addAction: addAction,
- hideSummaryPanel: hideSummaryPanel,
-
detailVisible: function () { return detail.panel().isVisible(); },
summaryVisible: function () { return summary.panel().isVisible(); }
};
diff --git a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
index deb49a5..535cd17 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
@@ -70,11 +70,12 @@
// initial toggle state: default settings and tag to key mapping
var defaultPrefsState = {
- summary: 1,
insts: 1,
+ summary: 1,
detail: 1,
hosts: 0,
offdev: 1,
+ dlbls: 0,
porthl: 1,
bg: 0,
spr: 0,
@@ -104,7 +105,7 @@
}
function setInitToggleState() {
- cachedState = ps.asNumbers(ps.getPrefs(cooktag));
+ cachedState = ps.asNumbers(ps.getPrefs(cooktag, defaultPrefsState));
$log.debug('TOOLBAR---- read prefs state:', cachedState);
if (!cachedState) {
@@ -264,6 +265,9 @@
function toggleToolbar() {
toolbar.toggle();
+ var prefs = ps.getPrefs(cooktag, defaultPrefsState);
+ prefs.toolbar = !prefs.toolbar;
+ ps.setPrefs('topo_prefs', prefs);
}
function setDefaultOverlay() {
@@ -298,6 +302,7 @@
keyListener: keyListener,
toggleToolbar: toggleToolbar,
setDefaultOverlay: setDefaultOverlay,
+ defaultPrefs: defaultPrefsState,
fnkey: fnkey
};
}]);
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index d014515..1a645ad 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -38,6 +38,9 @@
<script src="tp/Chart.min.js"></script>
<script src="tp/angular-chart.min.js"></script>
+ <!-- {INJECTED-USER-START} -->
+ <!-- {INJECTED-USER-END} -->
+
<!-- ONOS UI Framework included here -->
<!-- TODO: use a single catenated-minified file here -->
<script src="onos.js"></script>