thingsboard-aplcache
Changes
.travis.yml 9(+9 -0)
application/build.gradle 2(+1 -1)
application/pom.xml 5(+3 -2)
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java 26(+17 -9)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 14(+8 -6)
application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java 1(+0 -1)
application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java 3(+1 -2)
application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java 2(+2 -0)
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java 8(+4 -4)
application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java 5(+2 -3)
common/data/pom.xml 2(+1 -1)
common/message/pom.xml 2(+1 -1)
common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java 38(+38 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java 20(+19 -1)
common/pom.xml 2(+1 -1)
common/transport/pom.xml 2(+1 -1)
dao/pom.xml 2(+1 -1)
dao/src/main/resources/system-data.cql 18(+14 -4)
dao/src/test/resources/logback.xml 2(+1 -1)
docker/deploy_cassandra_zookeeper.sh 2(+1 -1)
docker/docker-compose.random.yml 4(+2 -2)
docker/docker-compose.static.yml 4(+2 -2)
docker/docker-compose.yml 10(+2 -8)
extensions/extension-kafka/pom.xml 2(+1 -1)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java 2(+1 -1)
extensions/pom.xml 2(+1 -1)
extensions-api/pom.xml 2(+1 -1)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java 36(+36 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java 2(+1 -1)
extensions-core/pom.xml 2(+1 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java 4(+3 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java 2(+1 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java 2(+1 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java 6(+3 -3)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 16(+8 -8)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java 2(+1 -1)
extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java 2(+1 -1)
pom.xml 2(+1 -1)
README.md 4(+3 -1)
tools/pom.xml 41(+39 -2)
tools/src/main/resources/logback.xml 34(+34 -0)
tools/test.properties 3(+3 -0)
transport/coap/pom.xml 2(+1 -1)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java 5(+4 -1)
transport/http/pom.xml 2(+1 -1)
transport/mqtt/pom.xml 2(+1 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java 3(+1 -2)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java 8(+6 -2)
transport/pom.xml 2(+1 -1)
ui/.eslintrc 3(+3 -0)
ui/package.json 20(+11 -9)
ui/pom.xml 2(+1 -1)
ui/src/app/app.js 2(+2 -0)
ui/src/app/app.run.js 3(+3 -0)
ui/src/app/components/dashboard.tpl.html 112(+57 -55)
ui/src/app/components/react/json-form-image.jsx 105(+105 -0)
ui/src/app/dashboard/dashboard.controller.js 29(+29 -0)
ui/src/app/dashboard/dashboard.tpl.html 14(+13 -1)
ui/src/app/dashboard/dashboard-settings.scss 91(+91 -0)
ui/src/app/dashboard/dashboard-settings.tpl.html 115(+115 -0)
ui/src/app/dashboard/index.js 2(+2 -0)
ui/src/app/device/device.directive.js 13(+11 -2)
ui/src/locale/en_US.json 23(+22 -1)
Details
.travis.yml 9(+9 -0)
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a7332e8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+before_install:
+ - sudo rm /etc/mavenrc
+ - export M2_HOME=/usr/local/maven
+ - export MAVEN_OPTS="-Dmaven.repo.local=$HOME/.m2/repository -Xms1024m -Xmx3072m"
+jdk:
+ - oraclejdk8
+language: java
+sudo: required
+script: mvn clean verify
application/build.gradle 2(+1 -1)
diff --git a/application/build.gradle b/application/build.gradle
index 8074ea1..ce8b10f 100644
--- a/application/build.gradle
+++ b/application/build.gradle
@@ -49,7 +49,7 @@ ospackage {
from(mainJar) {
// Strip the version from the jar filename
rename { String fileName ->
- fileName.replace("-${project.version}", "")
+ "${pkgName}.jar"
}
fileMode 0500
into "bin"
application/pom.xml 5(+3 -2)
diff --git a/application/pom.xml b/application/pom.xml
index 299a1c1..d0c7035 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
@@ -379,6 +379,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
+ <classifier>boot</classifier>
<layout>ZIP</layout>
<executable>true</executable>
<excludeDevtools>true</excludeDevtools>
@@ -408,7 +409,7 @@
<args>
<arg>-PprojectBuildDir=${project.build.directory}</arg>
<arg>-PprojectVersion=${project.version}</arg>
- <arg>-PmainJar=${project.build.directory}/${project.build.finalName}.${project.packaging}</arg>
+ <arg>-PmainJar=${project.build.directory}/${project.build.finalName}-boot.${project.packaging}</arg>
<arg>-PpkgName=${pkg.name}</arg>
<arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
<arg>-PpkgLogFolder=${pkg.logFolder}</arg>
diff --git a/application/src/main/conf/logback.xml b/application/src/main/conf/logback.xml
index 4187356..5d53400 100644
--- a/application/src/main/conf/logback.xml
+++ b/application/src/main/conf/logback.xml
@@ -24,7 +24,7 @@
<file>${pkg.logFolder}/${pkg.name}.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
- <fileNamePattern>${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+ <fileNamePattern>${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index c370616..8f05422 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -121,7 +121,7 @@ public class AppActor extends ContextAwareActor {
private void broadcast(Object msg) {
pluginManager.broadcast(msg);
- tenantActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ tenantActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
}
private void onToRuleMsg(ToRuleActorMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index 8b669e9..bf18df6 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.*;
@@ -58,6 +59,8 @@ public class DeviceActor extends ContextAwareActor {
processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
} else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
+ } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg){
+ processor.processCredentialsUpdate(context(), (DeviceCredentialsUpdateNotificationMsg) msg);
}
} else if (msg instanceof TimeoutMsg) {
processor.processTimeout(context(), (TimeoutMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 3949691..3aef0c8 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -32,13 +32,7 @@ import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
-import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
-import org.thingsboard.server.common.msg.core.BasicToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.core.SessionCloseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
@@ -47,6 +41,7 @@ import org.thingsboard.server.common.msg.session.SessionType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
import org.thingsboard.server.extensions.api.device.DeviceAttributes;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
@@ -74,6 +69,7 @@ import java.util.stream.Collectors;
public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
private final DeviceId deviceId;
+ private final Map<SessionId, SessionInfo> sessions;
private final Map<SessionId, SessionInfo> attributeSubscriptions;
private final Map<SessionId, SessionInfo> rpcSubscriptions;
@@ -85,6 +81,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
super(systemContext, logger);
this.deviceId = deviceId;
+ this.sessions = new HashMap<>();
this.attributeSubscriptions = new HashMap<>();
this.rpcSubscriptions = new HashMap<>();
this.rpcPendingMap = new HashMap<>();
@@ -281,7 +278,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (!msg.isAdded()) {
logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
- .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
+ .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
attributeSubscriptions.entrySet().removeIf(filter);
rpcSubscriptions.entrySet().removeIf(filter);
}
@@ -342,8 +339,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private void processSessionStateMsgs(ToDeviceActorMsg msg) {
SessionId sessionId = msg.getSessionId();
FromDeviceMsg inMsg = msg.getPayload();
- if (inMsg instanceof SessionCloseMsg) {
+ if (inMsg instanceof SessionOpenMsg) {
+ logger.debug("[{}] Processing new session [{}]", deviceId, sessionId);
+ sessions.put(sessionId, new SessionInfo(SessionType.ASYNC, msg.getServerAddress()));
+ } else if (inMsg instanceof SessionCloseMsg) {
logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
+ sessions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
rpcSubscriptions.remove(sessionId);
}
@@ -363,4 +364,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
}
+ public void processCredentialsUpdate(ActorContext context, DeviceCredentialsUpdateNotificationMsg msg) {
+ sessions.forEach((k, v) -> {
+ sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(new SessionCloseNotification(), k), v.getServer());
+ });
+ attributeSubscriptions.clear();
+ rpcSubscriptions.clear();
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
index 72ae4bb..c1d2678 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
@@ -181,7 +181,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
logger.info("[{}] Plugin requires restart due to clazz change from {} to {}.",
entityId, oldPluginMd.getClazz(), pluginMd.getClazz());
requiresRestart = true;
- } else if (oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
+ } else if (!oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
logger.info("[{}] Plugin requires restart due to configuration change from {} to {}.",
entityId, oldPluginMd.getConfiguration(), pluginMd.getConfiguration());
requiresRestart = true;
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
index 82011c0..1f64a92 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
@@ -234,18 +234,18 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd);
try {
fetchPluginInfo();
- if (!Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
+ if (filters == null || !Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
logger.info("[{}] Rule filters require restart due to json change from {} to {}.",
entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters()));
stopFilters();
initFilters();
}
- if (!Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
+ if (processor == null || !Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
logger.info("[{}] Rule processor require restart due to configuration change.", entityId);
stopProcessor();
initProcessor();
}
- if (!Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
+ if (action == null || !Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
logger.info("[{}] Rule action require restart due to configuration change.", entityId);
stopAction();
initAction();
@@ -272,13 +272,15 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
if (action != null) {
if (filters != null) {
filters.forEach(f -> f.resume());
+ } else {
+ initFilters();
}
if (processor != null) {
processor.resume();
+ } else {
+ initProcessor();
}
- if (action != null) {
- action.resume();
- }
+ action.resume();
logger.info("[{}] Rule resumed.", entityId);
} else {
start();
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
index 8112ac4..5a8b20a 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.actors.rule;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -26,7 +25,7 @@ public class SimpleRuleActorChain implements RuleActorChain {
public SimpleRuleActorChain(Set<RuleActorMetaData> ruleSet) {
rules = new ArrayList<>(ruleSet);
- Collections.sort(rules, RuleActorMetaData.RULE_ACTOR_MD_COMPARATOR);
+ rules.sort(RuleActorMetaData.RULE_ACTOR_MD_COMPARATOR);
}
public int size() {
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index 1c64f54..3db7210 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.actors.service;
+import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -28,4 +29,6 @@ public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor
void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+
+ void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index db6526d..bbf1300 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -32,16 +32,19 @@ import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
import org.thingsboard.server.actors.session.SessionManagerActor;
import org.thingsboard.server.actors.stats.StatsActor;
+import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
@@ -56,6 +59,7 @@ import scala.concurrent.duration.Duration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
+import java.util.Optional;
@Service
@Slf4j
@@ -221,6 +225,17 @@ public class DefaultActorService implements ActorService {
broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
}
+ @Override
+ public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
+ DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
+ Optional<ServerAddress> address = actorContext.getRoutingService().resolve(deviceId);
+ if (address.isPresent()) {
+ rpcService.tell(address.get(), msg);
+ } else {
+ onMsg(msg);
+ }
+ }
+
public void broadcast(ToAllNodesMsg msg) {
rpcService.broadcast(msg);
appActor.tell(msg, ActorRef.noSender());
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
index eb812df..916e678 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
@@ -20,15 +20,14 @@ import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
-import org.thingsboard.server.common.msg.core.ResponseMsg;
-import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
+import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.core.SessionCloseMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.session.*;
import akka.actor.ActorContext;
import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.msg.session.ctrl.*;
import org.thingsboard.server.common.msg.session.ex.SessionException;
import java.util.HashMap;
@@ -37,7 +36,8 @@ import java.util.Optional;
class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
- Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
+ private boolean firstMsg = true;
+ private Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
private Optional<ServerAddress> currentTargetServer;
private boolean subscribedToAttributeUpdates;
private boolean subscribedToRpcCommands;
@@ -49,6 +49,10 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
updateSessionCtx(msg, SessionType.ASYNC);
+ if (firstMsg) {
+ toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
+ firstMsg = false;
+ }
ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
switch (fromDeviceMsg.getMsgType()) {
@@ -80,17 +84,21 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
try {
- switch (msg.getMsgType()) {
- case STATUS_CODE_RESPONSE:
- case GET_ATTRIBUTES_RESPONSE:
- ResponseMsg responseMsg = (ResponseMsg) msg;
- if (responseMsg.getRequestId() >= 0) {
- logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
- pendingMap.remove(responseMsg.getRequestId());
- }
- break;
+ if (msg.getMsgType() != MsgType.SESSION_CLOSE) {
+ switch (msg.getMsgType()) {
+ case STATUS_CODE_RESPONSE:
+ case GET_ATTRIBUTES_RESPONSE:
+ ResponseMsg responseMsg = (ResponseMsg) msg;
+ if (responseMsg.getRequestId() >= 0) {
+ logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
+ pendingMap.remove(responseMsg.getRequestId());
+ }
+ break;
+ }
+ sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
+ } else {
+ sessionCtx.onMsg(org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg.onCredentialsRevoked(sessionCtx.getSessionId()));
}
- sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
} catch (SessionException e) {
logger.warning("Failed to push session response msg", e);
}
@@ -102,7 +110,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
}
protected void cleanupSession(ActorContext ctx) {
- toDeviceMsg(new SessionCloseMsg()).ifPresent(msg -> forwardToAppActor(ctx, msg));
+ toDeviceMsg(new SessionCloseMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
}
@Override
@@ -110,8 +118,9 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
if (!newTargetServer.equals(currentTargetServer)) {
+ firstMsg = true;
currentTargetServer = newTargetServer;
- pendingMap.values().stream().forEach(v -> {
+ pendingMap.values().forEach(v -> {
forwardToAppActor(context, v, currentTargetServer);
if (currentTargetServer.isPresent()) {
logger.debug("[{}] Forwarded msg to new server: {}", sessionId, currentTargetServer.get());
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
index 44eff16..c69946f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -66,7 +66,7 @@ public class SessionManagerActor extends ContextAwareActor {
}
private void broadcast(Object msg) {
- sessionActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ sessionActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
}
private void onSessionTimeout(SessionTimeoutMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
index afb35ac..9fb13d3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
@@ -52,7 +52,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
if (pendingResponse) {
try {
- sessionCtx.onMsg(new SessionCloseMsg(sessionId, true));
+ sessionCtx.onMsg(SessionCloseMsg.onTimeout(sessionId));
} catch (SessionException e) {
logger.warning("Failed to push session close msg", e);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
index 1c7f687..a3141ee 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -19,7 +19,6 @@ import akka.actor.ActorContext;
import akka.actor.ActorRef;
import akka.actor.Scheduler;
import akka.event.LoggingAdapter;
-import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
index c581c41..049accb 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
@@ -15,9 +15,9 @@
*/
package org.thingsboard.server.actors.shared.plugin;
-import java.util.HashMap;
-import java.util.Map;
-
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.plugin.PluginActor;
@@ -29,12 +29,9 @@ import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.dao.plugin.PluginService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import akka.actor.ActorContext;
-import akka.actor.ActorRef;
-import akka.actor.Props;
+import java.util.HashMap;
+import java.util.Map;
@Slf4j
public abstract class PluginManager {
@@ -64,17 +61,13 @@ public abstract class PluginManager {
abstract TenantId getTenantId();
public ActorRef getOrCreatePluginActor(ActorContext context, PluginId pluginId) {
- ActorRef pluginActor = pluginActors.get(pluginId);
- if (pluginActor == null) {
- pluginActor = context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pluginId))
- .withDispatcher(DefaultActorService.PLUGIN_DISPATCHER_NAME), pluginId.toString());
- pluginActors.put(pluginId, pluginActor);
- }
- return pluginActor;
+ return pluginActors.computeIfAbsent(pluginId, pId ->
+ context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pId))
+ .withDispatcher(DefaultActorService.PLUGIN_DISPATCHER_NAME), pId.toString()));
}
public void broadcast(Object msg) {
- pluginActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ pluginActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
}
public void remove(PluginId id) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
index d8b58a0..a27e903 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
@@ -20,7 +20,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.dao.plugin.BasePluginService;
-import org.thingsboard.server.dao.plugin.PluginService;
public class SystemPluginManager extends PluginManager {
@@ -30,7 +29,7 @@ public class SystemPluginManager extends PluginManager {
@Override
FetchFunction<PluginMetaData> getFetchPluginsFunction() {
- return link -> pluginService.findSystemPlugins(link);
+ return pluginService::findSystemPlugins;
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
index 67d44e9..dfe3f44 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
@@ -18,8 +18,7 @@ package org.thingsboard.server.actors.shared.rule;
import akka.actor.ActorContext;
import akka.actor.ActorRef;
import akka.actor.Props;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.rule.RuleActor;
import org.thingsboard.server.actors.rule.RuleActorChain;
@@ -38,10 +37,9 @@ import org.thingsboard.server.dao.rule.RuleService;
import java.util.*;
+@Slf4j
public abstract class RuleManager {
- protected static final Logger logger = LoggerFactory.getLogger(RuleManager.class);
-
protected final ActorSystemContext systemContext;
protected final RuleService ruleService;
protected final Map<RuleId, ActorRef> ruleActors;
@@ -63,11 +61,11 @@ public abstract class RuleManager {
ruleMap = new HashMap<>();
for (RuleMetaData rule : ruleIterator) {
- logger.debug("[{}] Creating rule actor {}", rule.getId(), rule);
+ log.debug("[{}] Creating rule actor {}", rule.getId(), rule);
ActorRef ref = getOrCreateRuleActor(context, rule.getId());
RuleActorMetaData actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref);
ruleMap.put(rule, actorMd);
- logger.debug("[{}] Rule actor created.", rule.getId());
+ log.debug("[{}] Rule actor created.", rule.getId());
}
refreshRuleChain();
@@ -79,8 +77,11 @@ public abstract class RuleManager {
rule = systemContext.getRuleService().findRuleById(ruleId);
}
if (rule == null) {
- rule = ruleMap.keySet().stream().filter(r -> r.getId().equals(ruleId)).findFirst().orElse(null);
- rule.setState(ComponentLifecycleState.SUSPENDED);
+ rule = ruleMap.keySet().stream()
+ .filter(r -> r.getId().equals(ruleId))
+ .peek(r -> r.setState(ComponentLifecycleState.SUSPENDED))
+ .findFirst()
+ .orElse(null);
}
if (rule != null) {
RuleActorMetaData actorMd = ruleMap.get(rule);
@@ -92,7 +93,7 @@ public abstract class RuleManager {
refreshRuleChain();
return Optional.of(actorMd.getActorRef());
} else {
- logger.warn("[{}] Can't process unknown rule!", rule.getId());
+ log.warn("[{}] Can't process unknown rule!", ruleId);
return Optional.empty();
}
}
@@ -100,13 +101,9 @@ public abstract class RuleManager {
abstract FetchFunction<RuleMetaData> getFetchRulesFunction();
public ActorRef getOrCreateRuleActor(ActorContext context, RuleId ruleId) {
- ActorRef ruleActor = ruleActors.get(ruleId);
- if (ruleActor == null) {
- ruleActor = context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, ruleId))
- .withDispatcher(DefaultActorService.RULE_DISPATCHER_NAME), ruleId.toString());
- ruleActors.put(ruleId, ruleActor);
- }
- return ruleActor;
+ return ruleActors.computeIfAbsent(ruleId, rId ->
+ context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, rId))
+ .withDispatcher(DefaultActorService.RULE_DISPATCHER_NAME), rId.toString()));
}
public RuleActorChain getRuleChain() {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
index 6d56832..7fac168 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
@@ -29,7 +29,7 @@ public class SystemRuleManager extends RuleManager {
@Override
FetchFunction<RuleMetaData> getFetchRulesFunction() {
- return link -> ruleService.findSystemRules(link);
+ return ruleService::findSystemRules;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index 965c652..c8d5243 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -100,7 +100,7 @@ public class TenantActor extends ContextAwareActor {
private void broadcast(Object msg) {
pluginManager.broadcast(msg);
- deviceActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ deviceActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
}
private void onToDeviceActorMsg(ToDeviceActorMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
index 99bec5b..f1ef6e1 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
@@ -18,12 +18,14 @@ package org.thingsboard.server.config;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
import org.springframework.context.support.ResourceBundleMessageSource;
@Configuration
public class ThingsboardMessageConfiguration {
@Bean
+ @Primary
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 7ae4604..ef365ca 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -134,7 +134,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
@Override
protected void configure(HttpSecurity http) throws Exception {
- http.headers().frameOptions().disable()
+ http.headers().cacheControl().disable().frameOptions().disable()
.and()
.csrf().disable()
.exceptionHandling()
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 1c0a7be..43416ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
@RestController
@RequestMapping("/api")
@@ -48,7 +49,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device", method = RequestMethod.POST)
- @ResponseBody
+ @ResponseBody
public Device saveDevice(@RequestBody Device device) throws ThingsboardException {
try {
device.setTenantId(getCurrentUser().getTenantId());
@@ -74,7 +75,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST)
- @ResponseBody
+ @ResponseBody
public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId,
@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
@@ -85,7 +86,7 @@ public class DeviceController extends BaseController {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId);
-
+
return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
} catch (Exception e) {
throw handleException(e);
@@ -94,7 +95,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE)
- @ResponseBody
+ @ResponseBody
public Device unassignDeviceFromCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
checkParameter("deviceId", strDeviceId);
try {
@@ -125,19 +126,21 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
- @ResponseBody
+ @ResponseBody
public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
checkNotNull(deviceCredentials);
try {
checkDeviceId(deviceCredentials.getDeviceId());
- return checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
+ DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
+ actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
+ return result;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
- @RequestMapping(value = "/tenant/devices", params = { "limit" }, method = RequestMethod.GET)
+ @RequestMapping(value = "/tenant/devices", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<Device> getTenantDevices(
@RequestParam int limit,
@@ -154,7 +157,7 @@ public class DeviceController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
- @RequestMapping(value = "/customer/{customerId}/devices", params = { "limit" }, method = RequestMethod.GET)
+ @RequestMapping(value = "/customer/{customerId}/devices", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<Device> getCustomerDevices(
@PathVariable("customerId") String strCustomerId,
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index e06962b..6d0f110 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -33,7 +33,7 @@ import java.util.List;
@RequestMapping("/api")
public class WidgetsBundleController extends BaseController {
- @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.GET)
@ResponseBody
public WidgetsBundle getWidgetsBundleById(@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@@ -76,7 +76,7 @@ public class WidgetsBundleController extends BaseController {
}
}
- @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundles", params = { "limit" }, method = RequestMethod.GET)
@ResponseBody
public TextPageData<WidgetsBundle> getWidgetsBundles(
@@ -97,7 +97,7 @@ public class WidgetsBundleController extends BaseController {
}
}
- @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET)
@ResponseBody
public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
index 29e9b3c..dd784f0 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
@@ -166,7 +166,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
publishCurrentServer();
- getOtherServers().stream().forEach(
+ getOtherServers().forEach(
server -> log.info("Found active server: [{}:{}]", server.getHost(), server.getPort())
);
}
@@ -194,13 +194,13 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
log.info("Processing [{}] event for [{}:{}]", pathChildrenCacheEvent.getType(), instance.getHost(), instance.getPort());
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
- listeners.stream().forEach(listener -> listener.onServerAdded(instance));
+ listeners.forEach(listener -> listener.onServerAdded(instance));
break;
case CHILD_UPDATED:
- listeners.stream().forEach(listener -> listener.onServerUpdated(instance));
+ listeners.forEach(listener -> listener.onServerUpdated(instance));
break;
case CHILD_REMOVED:
- listeners.stream().forEach(listener -> listener.onServerRemoved(instance));
+ listeners.forEach(listener -> listener.onServerRemoved(instance));
break;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
index 3c9ecf8..7a3c7ac 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
@@ -135,7 +135,7 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D
private void logCircle() {
log.trace("Consistent Hash Circle Start");
- circle.entrySet().stream().forEach((e) -> log.debug("{} -> {}", e.getKey(), e.getValue().getServerAddress()));
+ circle.entrySet().forEach((e) -> log.debug("{} -> {}", e.getKey(), e.getValue().getServerAddress()));
log.trace("Consistent Hash Circle End");
}
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index a51464c..975b52a 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -31,7 +31,6 @@ import org.thingsboard.server.dao.component.ComponentDescriptorService;
import org.thingsboard.server.extensions.api.component.*;
import javax.annotation.PostConstruct;
-import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.stream.Collectors;
@@ -72,7 +71,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
}
private void registerComponents(Collection<ComponentDescriptor> comps) {
- comps.stream().forEach(c -> components.put(c.getClazz(), c));
+ comps.forEach(c -> components.put(c.getClazz(), c));
}
private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
@@ -119,7 +118,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
throw new RuntimeException("Plugin " + def.getBeanClassName() + "action " + actionClazz.getName() + " has wrong component type!");
}
}
- scannedComponent.setActions(Arrays.asList(pluginAnnotation.actions()).stream().map(action -> action.getName()).collect(Collectors.joining(",")));
+ scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(action -> action.getName()).collect(Collectors.joining(",")));
break;
default:
throw new RuntimeException(type + " is not supported yet!");
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index b11517c..70949d2 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -26,6 +26,7 @@ import javax.mail.internet.MimeMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.velocity.app.VelocityEngine;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.thingsboard.server.exception.ThingsboardErrorCode;
import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.common.data.AdminSettings;
@@ -50,6 +51,7 @@ public class DefaultMailService implements MailService {
private MessageSource messages;
@Autowired
+ @Qualifier("velocityEngine")
private VelocityEngine engine;
private JavaMailSenderImpl mailSender;
@@ -101,6 +103,11 @@ public class DefaultMailService implements MailService {
throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
}
}
+
+ @Override
+ public void sendEmail(String email, String subject, String message) throws ThingsboardException {
+ sendMail(mailSender, mailFrom, email, subject, message);
+ }
@Override
public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
index e135253..9e44058 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
@@ -22,6 +22,8 @@ import com.fasterxml.jackson.databind.JsonNode;
public interface MailService {
void updateMailConfiguration();
+
+ void sendEmail(String email, String subject, String message) throws ThingsboardException;
void sendTestMail(JsonNode config, String email) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
index 83b87ab..6456968 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
@@ -20,9 +20,9 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.UserId;
-import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
public class SecurityUser extends User {
@@ -46,7 +46,7 @@ public class SecurityUser extends User {
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities == null) {
- authorities = Arrays.asList(SecurityUser.this.getAuthority()).stream()
+ authorities = Stream.of(SecurityUser.this.getAuthority())
.map(authority -> new SimpleGrantedAuthority(authority.name()))
.collect(Collectors.toList());
}
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
index 5506578..46c5a38 100644
--- a/application/src/main/resources/logback.xml
+++ b/application/src/main/resources/logback.xml
@@ -25,7 +25,7 @@
</encoder>
</appender>
- <logger name="org.thingsboard.server" level="TRACE" />
+ <logger name="org.thingsboard.server" level="INFO" />
<logger name="akka" level="INFO" />
<root level="INFO">
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index 538e8f9..20ceb77 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -129,8 +129,10 @@ public abstract class AbstractControllerTest {
@Autowired
void setConverters(HttpMessageConverter<?>[] converters) {
- this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
- hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
+ this.mappingJackson2HttpMessageConverter = Arrays.stream(converters)
+ .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
+ .findAny()
+ .get();
Assert.assertNotNull("the JSON message converter must not be null",
this.mappingJackson2HttpMessageConverter);
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java
index 8d22343..db90b89 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java
@@ -61,8 +61,10 @@ public class AbstractFeatureIntegrationTest {
@Autowired
void setConverters(HttpMessageConverter<?>[] converters) {
- this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
- hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
+ this.mappingJackson2HttpMessageConverter = Arrays.stream(converters)
+ .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
+ .findAny()
+ .get();
assertNotNull("the JSON message converter must not be null",
this.mappingJackson2HttpMessageConverter);
diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml
index f32acec..2577ca1 100644
--- a/application/src/test/resources/logback.xml
+++ b/application/src/test/resources/logback.xml
@@ -7,7 +7,7 @@
</encoder>
</appender>
- <logger name="org.thingsboard.server" level="DEBUG"/>
+ <logger name="org.thingsboard.server" level="WARN"/>
<logger name="org.springframework" level="WARN"/>
<logger name="org.apache.cassandra" level="WARN"/>
<logger name="org.cassandraunit" level="INFO"/>
common/data/pom.xml 2(+1 -1)
diff --git a/common/data/pom.xml b/common/data/pom.xml
index af1da85..6ba5e01 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
common/message/pom.xml 2(+1 -1)
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 5bc54b0..28144f0 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
new file mode 100644
index 0000000..3e96e40
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.common.msg.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+@ToString
+public class SessionCloseNotification implements ToDeviceMsg {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public boolean isSuccess() {
+ return true;
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SESSION_CLOSE;
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
new file mode 100644
index 0000000..d18dc9f
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class SessionOpenMsg implements FromDeviceMsg {
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SESSION_OPEN;
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
index d188527..03b611e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
@@ -21,11 +21,25 @@ import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
public class SessionCloseMsg implements SessionCtrlMsg {
private final SessionId sessionId;
+ private final boolean revoked;
private final boolean timeout;
- public SessionCloseMsg(SessionId sessionId, boolean timeout) {
+ public static SessionCloseMsg onError(SessionId sessionId) {
+ return new SessionCloseMsg(sessionId, false, false);
+ }
+
+ public static SessionCloseMsg onTimeout(SessionId sessionId) {
+ return new SessionCloseMsg(sessionId, false, true);
+ }
+
+ public static SessionCloseMsg onCredentialsRevoked(SessionId sessionId) {
+ return new SessionCloseMsg(sessionId, true, false);
+ }
+
+ private SessionCloseMsg(SessionId sessionId, boolean unauthorized, boolean timeout) {
super();
this.sessionId = sessionId;
+ this.revoked = unauthorized;
this.timeout = timeout;
}
@@ -34,6 +48,10 @@ public class SessionCloseMsg implements SessionCtrlMsg {
return sessionId;
}
+ public boolean isCredentialsRevoked() {
+ return revoked;
+ }
+
public boolean isTimeout() {
return timeout;
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
index 1b91425..549a143 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
@@ -28,7 +28,7 @@ public enum MsgType {
RULE_ENGINE_ERROR,
- SESSION_CLOSE;
+ SESSION_OPEN, SESSION_CLOSE;
private final boolean requiresRulesProcessing;
common/pom.xml 2(+1 -1)
diff --git a/common/pom.xml b/common/pom.xml
index bdbe1a8..77a44ac 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
common/transport/pom.xml 2(+1 -1)
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index b02cc07..8bcd650 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
dao/pom.xml 2(+1 -1)
diff --git a/dao/pom.xml b/dao/pom.xml
index 9ae2cae..de56032 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java
index 4c542e3..ce4dd81 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java
@@ -140,7 +140,7 @@ public class BaseAttributesDao extends AbstractDao implements AttributesDao {
List<Row> rows = resultSet.all();
List<AttributeKvEntry> entries = new ArrayList<>(rows.size());
if (!rows.isEmpty()) {
- rows.stream().forEach(row -> {
+ rows.forEach(row -> {
String key = row.getString(ModelConstants.ATTRIBUTE_KEY_COLUMN);
AttributeKvEntry kvEntry = convertResultToAttributesKvEntry(key, row);
if (kvEntry != null) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
index 79134f7..851c770 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
@@ -143,7 +143,7 @@ public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao {
public List<TsKvEntry> convertResultToTsKvEntryList(List<Row> rows) {
List<TsKvEntry> entries = new ArrayList<>(rows.size());
if (!rows.isEmpty()) {
- rows.stream().forEach(row -> {
+ rows.forEach(row -> {
TsKvEntry kvEntry = convertResultToTsKvEntry(row);
if (kvEntry != null) {
entries.add(kvEntry);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
index 17784d1..a3ab6bd 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -156,7 +156,7 @@ public class UserServiceImpl implements UserService {
UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByUserId(userEntity.getId());
UserCredentials userCredentials = getData(userCredentialsEntity);
if (!userCredentials.isEnabled()) {
- throw new IncorrectParameterException("Unable to reset password for unactive user");
+ throw new IncorrectParameterException("Unable to reset password for inactive user");
}
userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(30));
return saveUserCredentials(userCredentials);
dao/src/main/resources/system-data.cql 18(+14 -4)
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index 1358e58..06b9351 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -73,6 +73,11 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'simple_card',
'Simple card' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'label_widget',
+'{"type":"latest","sizeX":4.5,"sizeY":5,"resources":[],"templateHtml":"","templateCss":"#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}","controllerScript":"var bImageHeight;\nvar bImageWidth;\nvar backgroundRect;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\nvar labels;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var container = $(containerElement);\n var imageUrl = settings.backgroundImageUrl ? settings.backgroundImageUrl :\n ''data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=='';\n\n container.css(''background'', ''url(\"''+imageUrl+''\") no-repeat'');\n container.css(''backgroundSize'', ''contain'');\n container.css(''backgroundPosition'', ''50% 50%'');\n \n function processLabelPattern(pattern, data) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n labels = [];\n\n for (var l in configuredLabels) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : ''${#0}'';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : ''rgba(0,0,0,0)'';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || ''RobotoDraft'';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : ''normal'';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : ''500'';\n localConfig.font.color = settingsFont.color ? settingsFont.color : ''#fff'';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, data);\n \n var label = {};\n var labelElement = $(''<div/>'');\n labelElement.css(''position'', ''absolute'');\n labelElement.css(''top'', ''0'');\n labelElement.css(''left'', ''0'');\n labelElement.css(''backgroundColor'', localConfig.backgroundColor);\n labelElement.css(''color'', localConfig.font.color);\n labelElement.css(''fontFamily'', localConfig.font.family);\n labelElement.css(''fontStyle'', localConfig.font.style);\n labelElement.css(''fontWeight'', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n labels.push(label);\n }\n\n var bgImg = $(''<img />'');\n bgImg.hide();\n bgImg.bind(''load'', function()\n {\n bImageHeight = $(this).height();\n bImageWidth = $(this).width();\n });\n container.append(bgImg);\n bgImg.attr(''src'', imageUrl);\n \n units = settings.units || \"\";\n valueDec = (typeof settings.valueDec !== ''undefined'' && settings.valueDec !== null)\n ? settings.valueDec : 2;\n \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n }\n \n if (bImageHeight && bImageWidth) {\n if (sizeChanged || !backgroundRect) {\n backgroundRect = {};\n var imageRatio = bImageWidth / bImageHeight;\n var componentRatio = width / height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l in labels) {\n var label = labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css(''top'', labelTop + ''%'');\n label.element.css(''left'', labelLeft + ''%'');\n label.element.css(''fontSize'', fontSize + ''px'');\n }\n \n }\n }\n \n for (var l in labels) {\n var label = labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n label.element.html(text);\n }\n \n\n};\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"backgroundImageUrl\"],\n \"properties\": {\n \"backgroundImageUrl\": {\n \"title\": \"Background image\",\n \"type\": \"string\",\n \"default\": \"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\"\n },\n \"labels\": {\n \"title\": \"Labels\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Label\",\n \"type\": \"object\",\n \"required\": [\"pattern\"],\n \"properties\": {\n \"pattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"${#0}\"\n },\n \"x\": {\n \"title\": \"X (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"y\": {\n \"title\": \"Y (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"backgroundColor\": {\n \"title\": \"Backround color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0)\"\n },\n \"font\": {\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Relative font size (percents)\",\n \"type\": \"number\",\n \"default\": 6\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"form\": [\n {\n \"key\": \"backgroundImageUrl\",\n \"type\": \"image\"\n },\n {\n \"key\": \"labels\",\n \"items\": [\n \"labels[].pattern\",\n \"labels[].x\",\n \"labels[].y\",\n {\n \"key\": \"labels[].backgroundColor\",\n \"type\": \"color\"\n },\n \"labels[].font.family\",\n \"labels[].font.size\",\n {\n \"key\": \"labels[].font.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n\n },\n {\n \"key\": \"labels[].font.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labels[].font.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"RobotoDraft\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\"}"}',
+'Label widget' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'speed_gauge_canvas_gauges',
'{"type":"latest","sizeX":7,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"startAngle\": {\n \"title\": \"Start ticks angle\",\n \"type\": \"number\",\n \"default\": 45\n },\n \"ticksAngle\": {\n \"title\": \"Ticks angle\",\n \"type\": \"number\",\n \"default\": 270\n },\n \"needleCircleSize\": {\n \"title\": \"Needle circle size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"startAngle\",\n \"ticksAngle\",\n \"needleCircleSize\",\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge - Canvas Gauges\"}"}',
'Speed gauge - Canvas Gauges' );
@@ -123,11 +128,16 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'radial_gauge_canvas_gau
'Radial gauge - Canvas Gauges' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
-VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
-'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar markerCluster;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n };\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n return marker; \n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = new google.maps.LatLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getPosition();\n if (!prevPosition.equals(location)) {\n position.marker.setPosition(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = new google.maps.LatLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getPosition());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getPosition());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n this.setZoom(zoomLevel);\n if (!defaultZoomLevel && this.getZoom() > 15) {\n this.setZoom(15);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n var bounds = new google.maps.LatLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getPosition());\n }\n fitMapBounds(bounds);\n }\n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"Google Maps\"}"}',
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
+'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\",\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n \n markersSettings[i].tooltipPattern = configuredMarkersSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+markersSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+markersSettings[i].lngKeyName+\":7}\";\n \n markersSettings[i].tooltipReplaceInfo = procesTooltipPattern(markersSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n };\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker; \n }\n \n function createTooltip(marker, pattern, replaceInfo) {\n var infowindow = new google.maps.InfoWindow({\n content: ''''\n });\n marker.addListener(''click'', function() {\n infowindow.open(map, marker);\n });\n tooltips.push( {\n infowindow: infowindow,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = new google.maps.LatLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getPosition();\n if (!prevPosition.equals(location)) {\n position.marker.setPosition(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = new google.maps.LatLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getPosition());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getPosition());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n this.setZoom(zoomLevel);\n if (!defaultZoomLevel && this.getZoom() > 15) {\n this.setZoom(15);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n if (!dontFitMapBounds) {\n var bounds = new google.maps.LatLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getPosition());\n }\n fitMapBounds(bounds);\n }\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.infowindow.setContent(text);\n } \n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n \"markersSettings[].tooltipPattern\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"}],\"fitMapBounds\":true},\"title\":\"Google Maps\"}"}',
'Google Maps' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
+'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n \n var configuredRoutesSettings = settings.routesSettings;\n if (!configuredRoutesSettings) {\n configuredRoutesSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n routesSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name, \n color: \"#FE7569\",\n strokeWeight: 2,\n strokeOpacity: 1.0,\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredRoutesSettings[i]) {\n routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n \n routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color.substr(1);\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker; \n }\n \n function createTooltip(marker, pattern, replaceInfo) {\n var infowindow = new google.maps.InfoWindow({\n content: ''''\n });\n marker.addListener(''click'', function() {\n infowindow.open(map, marker);\n });\n tooltips.push( {\n infowindow: infowindow,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n\n function createPolyline(locations, settings) {\n var polyline = new google.maps.Polyline({\n path: locations,\n strokeColor: settings.color,\n strokeOpacity: settings.strokeOpacity,\n strokeWeight: settings.strokeWeight,\n map: map\n });\n \n return polyline; \n } \n \n function arraysEqual(a, b) {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a.length != b.length) return false;\n\n for (var i = 0; i < a.length; ++i) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n }\n \n \n function updateRoute(route, data) {\n if (route.latIndex > -1 && route.lngIndex > -1) {\n var latData = data[route.latIndex].data;\n var lngData = data[route.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var locations = [];\n for (var i = 0; i < latData.length; i++) {\n var lat = latData[i][1];\n var lng = lngData[i][1];\n var location = new google.maps.LatLng(lat, lng);\n locations.push(location);\n }\n var markerLocation;\n if (locations.length > 0) {\n markerLocation = locations[locations.length-1];\n }\n if (!route.polyline) {\n route.polyline = createPolyline(locations, route.settings);\n if (markerLocation) {\n route.marker = createMarker(markerLocation, route.settings);\n }\n polylines.push(route.polyline);\n return true;\n } else {\n var prevPath = route.polyline.getPath();\n if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n route.polyline.setPath(locations);\n if (markerLocation) {\n if (!route.marker) {\n route.marker = createMarker(markerLocation, route.settings);\n } else {\n route.marker.setPosition(markerLocation);\n }\n }\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function extendBounds(bounds, polyline) {\n if (polyline && polyline.getPath()) {\n var locations = polyline.getPath();\n for (var i = 0; i < locations.getLength(); i++) {\n bounds.extend(locations.getAt(i));\n }\n }\n }\n \n function loadRoutes(data) {\n var bounds = new google.maps.LatLngBounds();\n routes = [];\n var datasourceIndex = -1;\n var routeSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n routeSettings = routesSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === routeSettings.latKeyName ||\n dataKey.label === routeSettings.lngKeyName) {\n var route = routes[datasourceIndex];\n if (!route) {\n route = {\n latIndex: -1,\n lngIndex: -1,\n settings: routeSettings\n };\n routes[datasourceIndex] = route;\n } else if (route.polyline) {\n continue;\n }\n if (dataKey.label === routeSettings.latKeyName) {\n route.latIndex = i;\n } else {\n route.lngIndex = i;\n }\n if (route.latIndex > -1 && route.lngIndex > -1) {\n updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n \n function updateRoutes(data) {\n var routesChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var r in routes) {\n var route = routes[r];\n routesChanged |= updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n if (!dontFitMapBounds && routesChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n this.setZoom(zoomLevel);\n if (!defaultZoomLevel && this.getZoom() > 15) {\n this.setZoom(15);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!routes) {\n loadRoutes(data);\n } else {\n updateRoutes(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n if (!dontFitMapBounds) {\n var bounds = new google.maps.LatLngBounds();\n for (var p in polylines) {\n extendBounds(bounds, polylines[p]);\n }\n fitMapBounds(bounds);\n }\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.infowindow.setContent(text);\n }\n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all routes\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":false,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}],\"defaultZoomLevel\":16},\"title\":\"Route Map\"}"}',
+'Route Map' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
'{"type":"timeseries","sizeX":8,"sizeY":6,"resources":[{"url":"https://rawgithub.com/HumbleSoftware/Flotr2/master/flotr2.min.js"}],"templateHtml":"","templateCss":"","controllerScript":"var graph, options;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var colors = [];\n for (var i in data) {\n data[i].label = data[i].dataKey.label;\n colors.push(data[i].dataKey.color);\n var keySettings = data[i].dataKey.settings;\n\n data[i].lines = {\n fill: keySettings.fillLines || false,\n show: keySettings.showLines || true\n };\n\n data[i].points = {\n show: keySettings.showPoints || false\n };\n }\n options = {\n colors: colors,\n title: null,\n subtitle: null,\n shadowSize: settings.shadowSize || 4,\n fontColor: settings.fontColor || \"#545454\",\n fontSize: settings.fontSize || 7.5,\n xaxis: {\n mode: ''time'',\n timeMode: ''local''\n },\n yaxis: {\n },\n HtmlText: false,\n grid: {\n verticalLines: true,\n horizontalLines: true\n }\n };\n if (settings.grid) {\n options.grid.color = settings.grid.color || \"#545454\";\n options.grid.backgroundColor = settings.grid.backgroundColor || null;\n options.grid.tickColor = settings.grid.tickColor || \"#DDDDDD\";\n options.grid.verticalLines = settings.grid.verticalLines !== false;\n options.grid.horizontalLines = settings.grid.horizontalLines !== false;\n }\n if (settings.xaxis) {\n options.xaxis.showLabels = settings.xaxis.showLabels !== false;\n options.xaxis.color = settings.xaxis.color || null;\n options.xaxis.title = settings.xaxis.title || null;\n options.xaxis.titleAngle = settings.xaxis.titleAngle || 0;\n }\n if (settings.yaxis) {\n options.yaxis.showLabels = settings.yaxis.showLabels !== false;\n options.yaxis.color = settings.yaxis.color || null;\n options.yaxis.title = settings.yaxis.title || null;\n options.yaxis.titleAngle = settings.yaxis.titleAngle || 0;\n }\n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n options.xaxis.min = timeWindow.minTime;\n options.xaxis.max = timeWindow.maxTime;\n graph = Flotr.draw(containerElement, data, options);\n};\n\nfns.destroy = function() {\n //console.log(''destroy!'');\n};","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"shadowSize\": {\n \"title\": \"Shadow size\",\n \"type\": \"number\",\n \"default\": 4\n },\n \"fontColor\": {\n \"title\": \"Font color\",\n \"type\": \"string\",\n \"default\": \"#545454\"\n },\n \"fontSize\": {\n \"title\": \"Font size\",\n \"type\": \"number\",\n \"default\": 7.5\n },\n \"grid\": {\n \"title\": \"Grid settings\",\n \"type\": \"object\",\n \"properties\": {\n \"color\": {\n \"title\": \"Primary color\",\n \"type\": \"string\",\n \"default\": \"#545454\"\n },\n \"backgroundColor\": {\n \"title\": \"Background color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"tickColor\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": \"#DDDDDD\"\n },\n \"verticalLines\": {\n \"title\": \"Show vertical lines\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"horizontalLines\": {\n \"title\": \"Show horizontal lines\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"xaxis\": {\n \"title\": \"X axis settings\",\n \"type\": \"object\",\n \"properties\": {\n \"showLabels\": {\n \"title\": \"Show labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"title\": {\n \"title\": \"Axis title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"titleAngle\": {\n \"title\": \"Axis title''s angle in degrees\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"color\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"yaxis\": {\n \"title\": \"Y axis settings\",\n \"type\": \"object\",\n \"properties\": {\n \"showLabels\": {\n \"title\": \"Show labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"title\": {\n \"title\": \"Axis title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"titleAngle\": {\n \"title\": \"Axis title''s angle in degrees\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"color\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"shadowSize\", \n {\n \"key\": \"fontColor\",\n \"type\": \"color\"\n },\n \"fontSize\", \n {\n \"key\": \"grid\",\n \"items\": [\n {\n \"key\": \"grid.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"grid.backgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"grid.tickColor\",\n \"type\": \"color\"\n },\n \"grid.verticalLines\",\n \"grid.horizontalLines\"\n ]\n },\n {\n \"key\": \"xaxis\",\n \"items\": [\n \"xaxis.showLabels\",\n \"xaxis.title\",\n \"xaxis.titleAngle\",\n {\n \"key\": \"xaxis.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"yaxis\",\n \"items\": [\n \"yaxis.showLabels\",\n \"yaxis.title\",\n \"yaxis.titleAngle\",\n {\n \"key\": \"yaxis.color\",\n \"type\": \"color\"\n }\n ]\n }\n\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"showLines\": {\n \"title\": \"Show lines\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"fillLines\": {\n \"title\": \"Fill lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showPoints\": {\n \"title\": \"Show points\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\"showLines\", \"fillLines\", \"showPoints\"]\n },\n \"form\": [\n \"showLines\",\n \"fillLines\",\n \"showPoints\"\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":7.5,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"backgroundColor\":\"#ffffff\"}},\"title\":\"Timeseries - Flotr2\"}"}',
'Timeseries - Flotr2' );
@@ -193,8 +203,8 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_vertical_bar',
'Digital vertical bar' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
-VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
-'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar markerCluster;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n }\n \n map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n attribution: ''© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n\n var icon = L.icon({\n iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n iconSize: [21, 34],\n iconAnchor: [10, 34],\n popupAnchor: [0, -34],\n shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n shadowSize: [40, 37],\n shadowAnchor: [12, 35]\n });\n \n var marker = L.marker(location, {icon: icon}).addTo(map);\n marker.bindPopup(''<b>'' + settings.label + ''</b>'');\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n return marker;\n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = L.latLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getLatLng();\n if (!prevPosition.equals(location)) {\n position.marker.setLatLng(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = L.latLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = L.latLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n map.once(''zoomend'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n map.setZoom(zoomLevel, {animate: false});\n if (!defaultZoomLevel && this.getZoom() > 15) {\n map.setZoom(15, {animate: false});\n }\n });\n map.fitBounds(bounds, {padding: [50, 50], animate: false});\n }\n \n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n map.invalidateSize(true);\n var bounds = L.latLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getLatLng());\n }\n fitMapBounds(bounds);\n }\n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}"}',
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
+'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\",\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n \n markersSettings[i].tooltipPattern = configuredMarkersSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+markersSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+markersSettings[i].lngKeyName+\":7}\";\n \n markersSettings[i].tooltipReplaceInfo = procesTooltipPattern(markersSettings[i].tooltipPattern, datasources[i], datasourceOffset); \n \n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n \n map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n attribution: ''© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n\n var icon = L.icon({\n iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n iconSize: [21, 34],\n iconAnchor: [10, 34],\n popupAnchor: [0, -34],\n shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n shadowSize: [40, 37],\n shadowAnchor: [12, 35]\n });\n \n var marker = L.marker(location, {icon: icon}).addTo(map);\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker;\n }\n \n \n function createTooltip(marker, pattern, replaceInfo) {\n var popup = L.popup();\n popup.setContent('''');\n marker.bindPopup(popup);\n tooltips.push( {\n popup: popup,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = L.latLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getLatLng();\n if (!prevPosition.equals(location)) {\n position.marker.setLatLng(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = L.latLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = L.latLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n map.once(''zoomend'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n map.setZoom(zoomLevel, {animate: false});\n if (!defaultZoomLevel && this.getZoom() > 15) {\n map.setZoom(15, {animate: false});\n }\n });\n map.fitBounds(bounds, {padding: [50, 50], animate: false});\n }\n \n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n map.invalidateSize(true);\n var bounds = L.latLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getLatLng());\n }\n fitMapBounds(bounds);\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.popup.setContent(text);\n } \n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n \"markersSettings[].tooltipPattern\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}"}',
'OpenStreetMap' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
dao/src/test/resources/logback.xml 2(+1 -1)
diff --git a/dao/src/test/resources/logback.xml b/dao/src/test/resources/logback.xml
index 0969bbe..046d8f5 100644
--- a/dao/src/test/resources/logback.xml
+++ b/dao/src/test/resources/logback.xml
@@ -7,7 +7,7 @@
</encoder>
</appender>
- <logger name="org.thingsboard.server.dao" level="TRACE"/>
+ <logger name="org.thingsboard.server.dao" level="WARN"/>
<logger name="org.apache.cassandra" level="WARN"/>
<logger name="org.cassandraunit" level="INFO" />
<logger name="org.apache.cassandra" level="INFO" />
docker/deploy_cassandra_zookeeper.sh 2(+1 -1)
diff --git a/docker/deploy_cassandra_zookeeper.sh b/docker/deploy_cassandra_zookeeper.sh
index 263ef49..d2fd9c8 100755
--- a/docker/deploy_cassandra_zookeeper.sh
+++ b/docker/deploy_cassandra_zookeeper.sh
@@ -28,4 +28,4 @@ echo "building images.."
$command build
echo "starting cassandra, zookeeper, thingsboard-db-schema images..."
-$command up -d cassandra zookeeper thingsboard-db-schema
+$command up -d db zk thingsboard-db-schema
docker/docker-compose.random.yml 4(+2 -2)
diff --git a/docker/docker-compose.random.yml b/docker/docker-compose.random.yml
index 9b51901..d70107a 100644
--- a/docker/docker-compose.random.yml
+++ b/docker/docker-compose.random.yml
@@ -17,10 +17,10 @@
version: '2'
services:
- cassandra:
+ db:
ports:
- "9042"
- "9160"
- zookeeper:
+ zk:
ports:
- "2181"
docker/docker-compose.static.yml 4(+2 -2)
diff --git a/docker/docker-compose.static.yml b/docker/docker-compose.static.yml
index bdaf4eb..1661378 100644
--- a/docker/docker-compose.static.yml
+++ b/docker/docker-compose.static.yml
@@ -17,10 +17,10 @@
version: '2'
services:
- cassandra:
+ db:
ports:
- "9042:9042"
- "9160:9160"
- zookeeper:
+ zk:
ports:
- "2181:2181"
docker/docker-compose.yml 10(+2 -8)
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index b15b7fa..185d54f 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -23,24 +23,18 @@ services:
- "8080:8080"
- "1883:1883"
- "5683:5683"
- links:
- - cassandra:db
- - zookeeper:zk
- - thingsboard-db-schema:thingsboard-db-schema
env_file:
- thingsboard.env
entrypoint: ./run_thingsboard.sh
thingsboard-db-schema:
image: "thingsboard/thingsboard-db-schema:1.0"
- links:
- - cassandra:db
env_file:
- thingsboard-db-schema.env
entrypoint: ./install_schema.sh
- cassandra:
+ db:
image: "cassandra:3.9"
volumes:
- "${CASSANDRA_DATA_DIR}:/var/lib/cassandra"
- zookeeper:
+ zk:
image: "zookeeper:3.4.9"
restart: always
extensions/extension-kafka/pom.xml 2(+1 -1)
diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index 6070d0b..431482b 100644
--- a/extensions/extension-kafka/pom.xml
+++ b/extensions/extension-kafka/pom.xml
@@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java b/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java
index 1642fb5..7321ad7 100644
--- a/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java
+++ b/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java
@@ -47,7 +47,7 @@ public class KafkaPlugin extends AbstractPlugin<KafkaPluginConfiguration> {
properties.put("buffer.memory", configuration.getBufferMemory());
if (configuration.getOtherProperties() != null) {
configuration.getOtherProperties()
- .stream().forEach(p -> properties.put(p.getKey(), p.getValue()));
+ .forEach(p -> properties.put(p.getKey(), p.getValue()));
}
init();
}
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index c764f60..99167bd 100644
--- a/extensions/extension-rabbitmq/pom.xml
+++ b/extensions/extension-rabbitmq/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml
index 59428e2..39072ee 100644
--- a/extensions/extension-rest-api-call/pom.xml
+++ b/extensions/extension-rest-api-call/pom.xml
@@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
extensions/pom.xml 2(+1 -1)
diff --git a/extensions/pom.xml b/extensions/pom.xml
index e825d1b..fc48d41 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
extensions-api/pom.xml 2(+1 -1)
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index ed4dd2d..aad2469 100644
--- a/extensions-api/pom.xml
+++ b/extensions-api/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java
new file mode 100644
index 0000000..0104824
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.extensions.api.device;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+
+import java.util.Set;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class DeviceCredentialsUpdateNotificationMsg implements ToDeviceActorNotificationMsg {
+
+ private final TenantId tenantId;
+ private final DeviceId deviceId;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
index fab11bb..1633c7f 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
@@ -91,7 +91,7 @@ public class DefaultWebsocketMsgHandler implements WebsocketMsgHandler {
}
public void clear(PluginContext ctx) {
- wsSessionsMap.values().stream().forEach(v -> {
+ wsSessionsMap.values().forEach(v -> {
try {
ctx.close(v.getSessionRef());
} catch (IOException e) {
extensions-core/pom.xml 2(+1 -1)
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index d8581e6..f4e7697 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
index 21180d7..7123f3e 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
@@ -40,7 +40,9 @@ public class MethodNameFilter extends SimpleRuleLifecycleComponent implements Ru
@Override
public void init(MethodNameFilterConfiguration configuration) {
- methods = Arrays.asList(configuration.getMethodNames()).stream().map(m -> m.getName()).collect(Collectors.toSet());
+ methods = Arrays.stream(configuration.getMethodNames())
+ .map(m -> m.getName())
+ .collect(Collectors.toSet());
}
@Override
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
index 84deea5..737bee6 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
@@ -39,7 +39,7 @@ public class MsgTypeFilter extends SimpleRuleLifecycleComponent implements RuleF
@Override
public void init(MsgTypeFilterConfiguration configuration) {
- msgTypes = Arrays.asList(configuration.getMessageTypes()).stream().map(type -> {
+ msgTypes = Arrays.stream(configuration.getMessageTypes()).map(type -> {
switch (type) {
case "GET_ATTRIBUTES":
return MsgType.GET_ATTRIBUTES_REQUEST;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
index 52fd2e9..c8a7ad8 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
@@ -75,7 +75,7 @@ public class MailPlugin extends AbstractPlugin<MailPluginConfiguration> implemen
if (configuration.getOtherProperties() != null) {
Properties mailProperties = new Properties();
configuration.getOtherProperties()
- .stream().forEach(p -> mailProperties.put(p.getKey(), p.getValue()));
+ .forEach(p -> mailProperties.put(p.getKey(), p.getValue()));
mail.setJavaMailProperties(mailProperties);
}
mailSender = mail;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
index b166dae..06467fe 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
@@ -97,7 +97,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
builder.setDeviceId(cmd.getDeviceId().toString());
builder.setType(cmd.getType().name());
builder.setAllKeys(cmd.isAllKeys());
- cmd.getKeyStates().entrySet().stream().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
+ cmd.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLAZZ, builder.build().toByteArray()));
}
@@ -144,7 +144,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
if (update.getErrorMsg() != null) {
builder.setErrorMsg(update.getErrorMsg());
}
- update.getData().entrySet().stream().forEach(
+ update.getData().entrySet().forEach(
e -> {
SubscriptionUpdateValueListProto.Builder dataBuilder = SubscriptionUpdateValueListProto.newBuilder();
@@ -166,7 +166,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
return new SubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
} else {
Map<String, List<Object>> data = new TreeMap<>();
- proto.getDataList().stream().forEach(v -> {
+ proto.getDataList().forEach(v -> {
List<Object> values = data.get(v.getKey());
if (values == null) {
values = new ArrayList<>();
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
index 6ea7489..8e2d62a 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
@@ -109,8 +109,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(keys.size());
- keys.stream().forEach(key -> subState.put(key, 0L));
- attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ keys.forEach(key -> subState.put(key, 0L));
+ attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
} else {
@@ -119,7 +119,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(attributesData.size());
- attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
}
@@ -154,8 +154,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size());
- keys.stream().forEach(key -> subState.put(key, startTs));
- data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ keys.forEach(key -> subState.put(key, startTs));
+ data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
} else {
@@ -168,8 +168,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size());
- keys.stream().forEach(key -> subState.put(key, startTs));
- data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ keys.forEach(key -> subState.put(key, startTs));
+ data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@@ -188,7 +188,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(data.size());
- data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, true, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
index 637500e..190d9ff 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
@@ -68,7 +68,7 @@ public class SubscriptionManager {
registerSubscription(sessionId, deviceId, subscription);
List<TsKvEntry> missedUpdates = new ArrayList<>();
if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
- subscription.getKeyStates().entrySet().stream().forEach(e -> {
+ subscription.getKeyStates().entrySet().forEach(e -> {
Optional<AttributeKvEntry> latestOpt = ctx.loadAttribute(deviceId, DataConstants.CLIENT_SCOPE, e.getKey());
if (latestOpt.isPresent()) {
AttributeKvEntry latestEntry = latestOpt.get();
diff --git a/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
index 3095f5d..bcaeeb5 100644
--- a/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
+++ b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
@@ -71,7 +71,7 @@ public class DeviceAttributesFilterTest {
filter.stop();
}
- @Test(timeout = 10000)
+ @Test(timeout = 30000)
public void basicClientAttributesStressTest() {
DeviceAttributesFilter filter = new DeviceAttributesFilter();
filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 59bc9e2..8e9ac9f 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<packaging>pom</packaging>
<name>Thingsboard</name>
README.md 4(+3 -1)
diff --git a/README.md b/README.md
index 4b08ffd..e308800 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# Thingsboard
+# Thingsboard
+[](https://gitter.im/thingsboard/chat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://travis-ci.org/thingsboard/thingsboard)
Thingsboard is an open-source IoT platform for data collection, processing, visualization, and device management.
tools/pom.xml 41(+39 -2)
diff --git a/tools/pom.xml b/tools/pom.xml
index 4134fc5..4ef32d4 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
@@ -78,6 +78,43 @@
<artifactId>mockito-all</artifactId>
<scope>test</scope>
</dependency>
-
</dependencies>
+
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <transformers>
+ <transformer
+ implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <manifestEntries>
+ <Main-Class>org.thingsboard.client.tools.MqttStressTestTool</Main-Class>
+ </manifestEntries>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestClient.java b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestClient.java
index b0ddf73..5805cad 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestClient.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestClient.java
@@ -40,10 +40,10 @@ public class MqttStressTestClient {
this.client = new MqttAsyncClient(brokerUri, clientId, persistence);
}
- public void connect() throws MqttException {
+ public IMqttToken connect() throws MqttException {
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(deviceToken);
- client.connect(options, null, new IMqttActionListener() {
+ return client.connect(options, null, new IMqttActionListener() {
@Override
public void onSuccess(IMqttToken iMqttToken) {
log.info("OnSuccess");
@@ -60,6 +60,22 @@ public class MqttStressTestClient {
client.disconnect();
}
+
+
+ public void warmUp(byte[] data) throws MqttException {
+ MqttMessage msg = new MqttMessage(data);
+ client.publish("v1/devices/me/telemetry", msg, null, new IMqttActionListener() {
+ @Override
+ public void onSuccess(IMqttToken asyncActionToken) {
+ }
+
+ @Override
+ public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+ }
+ }).waitForCompletion();
+ }
+
+
public void publishTelemetry(byte[] data) throws MqttException {
long sendTime = System.currentTimeMillis();
MqttMessage msg = new MqttMessage(data);
@@ -67,14 +83,12 @@ public class MqttStressTestClient {
@Override
public void onSuccess(IMqttToken asyncActionToken) {
long ackTime = System.currentTimeMillis();
-// log.info("Delivery time: {}", ackTime - sendTime);
results.onResult(true, ackTime - sendTime);
}
@Override
public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
long failTime = System.currentTimeMillis();
-// log.info("Failure time: {}", failTime - sendTime);
results.onResult(false, failTime - sendTime);
}
});
diff --git a/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java
index 900d81f..ed6f42b 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java
@@ -1,4 +1,4 @@
-package org.thingsboard.client.tools; /**
+/**
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,14 +13,32 @@ package org.thingsboard.client.tools; /**
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.thingsboard.client.tools; /**
+ * Copyright © 2016 The Thingsboard Authors
+ * <p>
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+
import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.TimeUnit;
+import java.util.UUID;
+import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -29,60 +47,83 @@ import java.util.concurrent.atomic.AtomicLong;
@Slf4j
public class MqttStressTestTool {
- private static final long TEST_DURATION = TimeUnit.MINUTES.toMillis(1);
- private static final long TEST_ITERATION = TimeUnit.MILLISECONDS.toMillis(100);
- private static final long TEST_SUB_ITERATION = TimeUnit.MILLISECONDS.toMillis(2);
- private static final int DEVICE_COUNT = 100;
- private static final String BASE_URL = "http://localhost:8080";
- private static final String[] MQTT_URLS = {"tcp://localhost:1883"};
-// private static final String[] MQTT_URLS = {"tcp://localhost:1883", "tcp://localhost:1884", "tcp://localhost:1885"};
- private static final String USERNAME = "tenant@thingsboard.org";
- private static final String PASSWORD = "tenant";
+ public static void main(String[] args) throws Exception {
+ TestParams params = new TestParams();
+ ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
- public static void main(String[] args) throws Exception {
+ if (params.getDuration() % params.getIterationInterval() != 0) {
+ throw new IllegalArgumentException("Test Duration % Iteration Interval != 0");
+ }
+
+ if ((params.getIterationInterval() * 1000) % params.getDeviceCount() != 0) {
+ throw new IllegalArgumentException("Iteration Interval % Device Count != 0");
+ }
+
ResultAccumulator results = new ResultAccumulator();
AtomicLong value = new AtomicLong(Long.MAX_VALUE);
log.info("value: {} ", value.incrementAndGet());
- RestClient restClient = new RestClient(BASE_URL);
- restClient.login(USERNAME, PASSWORD);
+ RestClient restClient = new RestClient(params.getRestApiUrl());
+ restClient.login(params.getUsername(), params.getPassword());
List<MqttStressTestClient> clients = new ArrayList<>();
- for (int i = 0; i < DEVICE_COUNT; i++) {
- Device device = restClient.createDevice("Device " + i);
+ List<IMqttToken> connectTokens = new ArrayList<>();
+ for (int i = 0; i < params.getDeviceCount(); i++) {
+ Device device = restClient.createDevice("Device " + UUID.randomUUID());
DeviceCredentials credentials = restClient.getCredentials(device.getId());
- String mqttURL = MQTT_URLS[i % MQTT_URLS.length];
+ String[] mqttUrls = params.getMqttUrls();
+ String mqttURL = mqttUrls[i % mqttUrls.length];
MqttStressTestClient client = new MqttStressTestClient(results, mqttURL, credentials.getCredentialsId());
- client.connect();
+ connectTokens.add(client.connect());
clients.add(client);
}
- Thread.sleep(1000);
+ for (IMqttToken tokens : connectTokens) {
+ tokens.waitForCompletion();
+ }
byte[] data = "{\"longKey\":73}".getBytes(StandardCharsets.UTF_8);
- long startTime = System.currentTimeMillis();
- int iterationsCount = (int) (TEST_DURATION / TEST_ITERATION);
- int subIterationsCount = (int) (TEST_ITERATION / TEST_SUB_ITERATION);
- if (clients.size() % subIterationsCount != 0) {
- throw new IllegalArgumentException("Invalid parameter exception!");
+
+ for (MqttStressTestClient client : clients) {
+ client.warmUp(data);
}
+
+ Thread.sleep(1000);
+
+ long startTime = System.currentTimeMillis();
+ int iterationsCount = (int) (params.getDuration() / params.getIterationInterval());
+ int subIterationMicroSeconds = (int) ((params.getIterationInterval() * 1000) / params.getDeviceCount());
+
+ List<ScheduledFuture<Void>> iterationFutures = new ArrayList<>();
for (int i = 0; i < iterationsCount; i++) {
- for (int j = 0; j < subIterationsCount; j++) {
- int packSize = clients.size() / subIterationsCount;
- for (int k = 0; k < packSize; k++) {
- int clientIndex = packSize * j + k;
- clients.get(clientIndex).publishTelemetry(data);
+ long delay = i * params.getIterationInterval();
+ iterationFutures.add(scheduler.schedule((Callable<Void>) () -> {
+ long sleepMicroSeconds = 0L;
+ for (MqttStressTestClient client : clients) {
+ client.publishTelemetry(data);
+ sleepMicroSeconds += subIterationMicroSeconds;
+ if (sleepMicroSeconds > 1000) {
+ Thread.sleep(sleepMicroSeconds / 1000);
+ sleepMicroSeconds = sleepMicroSeconds % 1000;
+ }
}
- Thread.sleep(TEST_SUB_ITERATION);
- }
+ return null;
+ }, delay, TimeUnit.MILLISECONDS));
}
+
+ for (ScheduledFuture<Void> future : iterationFutures) {
+ future.get();
+ }
+
Thread.sleep(1000);
+
for (MqttStressTestClient client : clients) {
client.disconnect();
}
log.info("Results: {} took {}ms", results, System.currentTimeMillis() - startTime);
+ scheduler.shutdownNow();
}
}
diff --git a/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java b/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java
index 5bba82a..1364fc1 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java
@@ -73,7 +73,7 @@ public class ResultAccumulator {
@Override
public String toString() {
- return "org.thingsboard.client.tools.ResultAccumulator{" +
+ return "Result {" +
"successCount=" + getSuccessCount() +
", errorCount=" + getErrorCount() +
", totalTime=" + getTimeSpent() +
diff --git a/tools/src/main/java/org/thingsboard/client/tools/TestParams.java b/tools/src/main/java/org/thingsboard/client/tools/TestParams.java
new file mode 100644
index 0000000..eb1328b
--- /dev/null
+++ b/tools/src/main/java/org/thingsboard/client/tools/TestParams.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.thingsboard.client.tools;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class TestParams {
+ static final String TEST_PROPERTIES = "test.properties";
+ static final long DEFAULT_TEST_DURATION = TimeUnit.MINUTES.toMillis(1);
+ static final long DEFAULT_TEST_INTERVAL = TimeUnit.MILLISECONDS.toMillis(100);
+ static final int DEFAULT_DEVICE_COUNT = 100;
+ static final String DEFAULT_REST_URL = "http://localhost:8080";
+ static final String DEFAULT_MQTT_URLS = "tcp://localhost:1883";
+ static final String DEFAULT_USERNAME = "tenant@thingsboard.org";
+ static final String DEFAULT_PASSWORD = "tenant";
+
+ private Properties params = new Properties();
+
+ public TestParams() throws IOException {
+ try {
+ params.load(new FileInputStream(TEST_PROPERTIES));
+ } catch (Exception e) {
+ log.warn("Failed to read " + TEST_PROPERTIES);
+ }
+ }
+
+ public long getDuration() {
+ return Long.valueOf(params.getProperty("durationMs", Long.toString(DEFAULT_TEST_DURATION)));
+ }
+
+ public long getIterationInterval() {
+ return Long.valueOf(params.getProperty("iterationIntervalMs", Long.toString(DEFAULT_TEST_INTERVAL)));
+ }
+
+ public int getDeviceCount() {
+ return Integer.valueOf(params.getProperty("deviceCount", Integer.toString(DEFAULT_DEVICE_COUNT)));
+ }
+
+ public String getRestApiUrl() {
+ return params.getProperty("restUrl", DEFAULT_REST_URL);
+ }
+
+ public String[] getMqttUrls() {
+ return params.getProperty("mqttUrls", DEFAULT_MQTT_URLS).split(",");
+ }
+
+ public String getUsername() {
+ return params.getProperty("username", DEFAULT_USERNAME);
+ }
+
+ public String getPassword() {
+ return params.getProperty("password", DEFAULT_PASSWORD);
+ }
+}
tools/src/main/resources/logback.xml 34(+34 -0)
diff --git a/tools/src/main/resources/logback.xml b/tools/src/main/resources/logback.xml
new file mode 100644
index 0000000..11973fa
--- /dev/null
+++ b/tools/src/main/resources/logback.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ 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.
+
+-->
+<!DOCTYPE configuration>
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.thingsboard" level="INFO" />
+
+ <root level="INFO">
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration>
\ No newline at end of file
diff --git a/tools/src/main/resources/test.properties b/tools/src/main/resources/test.properties
new file mode 100644
index 0000000..6e9ed89
--- /dev/null
+++ b/tools/src/main/resources/test.properties
@@ -0,0 +1,5 @@
+restUrl=http://localhost:8080
+mqttUrls=tcp://localhost:1883
+deviceCount=1
+durationMs=60000
+iterationIntervalMs=1000
tools/test.properties 3(+3 -0)
diff --git a/tools/test.properties b/tools/test.properties
new file mode 100644
index 0000000..93efc60
--- /dev/null
+++ b/tools/test.properties
@@ -0,0 +1,3 @@
+deviceCount=1000
+durationMs=5000
+iterationIntervalMs=250
transport/coap/pom.xml 2(+1 -1)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 6221596..09fb573 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
index a61f0bf..6f6e35a 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
@@ -36,6 +36,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
+
@Slf4j
public class CoapSessionCtx extends DeviceAwareSessionContext {
@@ -87,6 +88,8 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
private void onSessionClose(SessionCloseMsg msg) {
if (msg.isTimeout()) {
exchange.respond(ResponseCode.SERVICE_UNAVAILABLE);
+ } else if (msg.isCredentialsRevoked()) {
+ exchange.respond(ResponseCode.UNAUTHORIZED);
} else {
exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
}
@@ -120,7 +123,7 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
public void close() {
log.info("[{}] Closing processing context. Timeout: {}", sessionId, exchange.advanced().isTimedOut());
- processor.process(new SessionCloseMsg(sessionId, exchange.advanced().isTimedOut()));
+ processor.process(exchange.advanced().isTimedOut() ? SessionCloseMsg.onTimeout(sessionId) : SessionCloseMsg.onError(sessionId));
}
@Override
transport/http/pom.xml 2(+1 -1)
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 4a5fc78..d1b68a5 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
transport/mqtt/pom.xml 2(+1 -1)
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 9d332be..d5826b0 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index e1bb45c..b4d8108 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -210,7 +210,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
private void processDisconnect(ChannelHandlerContext ctx) {
- processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
ctx.close();
}
@@ -255,6 +254,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
- processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
+ processor.process(SessionCloseMsg.onError(sessionCtx.getSessionId()));
}
}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
index 5cae9f5..f653682 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
@@ -16,12 +16,13 @@
package org.thingsboard.server.transport.mqtt.session;
import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.*;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
import org.thingsboard.server.common.msg.session.ex.SessionException;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -75,7 +76,10 @@ public class MqttSessionCtx extends DeviceAwareSessionContext {
@Override
public void onMsg(SessionCtrlMsg msg) throws SessionException {
-
+ if (msg instanceof SessionCloseMsg) {
+ pushToNetwork(new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)));
+ channel.close();
+ }
}
@Override
transport/pom.xml 2(+1 -1)
diff --git a/transport/pom.xml b/transport/pom.xml
index a03dd83..ac12461 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/.eslintrc 3(+3 -0)
diff --git a/ui/.eslintrc b/ui/.eslintrc
index 5cb89e5..5b8920d 100644
--- a/ui/.eslintrc
+++ b/ui/.eslintrc
@@ -11,5 +11,8 @@
"node_modules",
"\\.tpl\\.html$"
]
+ },
+ "globals": {
+ "FileReader": true
}
}
ui/package.json 20(+11 -9)
diff --git a/ui/package.json b/ui/package.json
index d8978a7..c1101d2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard",
"private": true,
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Thingsboard UI",
"licenses": [
{
@@ -14,13 +14,14 @@
"build": "NODE_ENV=production webpack -p"
},
"dependencies": {
+ "@flowjs/ng-flow": "^2.7.1",
"ace-builds": "^1.2.5",
- "angular": "^1.5.8",
- "angular-animate": "^1.5.8",
- "angular-aria": "^1.5.8",
+ "angular": "1.5.8",
+ "angular-animate": "1.5.8",
+ "angular-aria": "1.5.8",
"angular-breadcrumb": "^0.4.1",
"angular-carousel": "^1.0.1",
- "angular-cookies": "^1.5.8",
+ "angular-cookies": "1.5.8",
"angular-drag-and-drop-lists": "^1.4.0",
"angular-fullscreen": "git://github.com/fabiobiondi/angular-fullscreen.git#master",
"angular-gridster": "^0.13.14",
@@ -29,11 +30,11 @@
"angular-material": "^1.1.1",
"angular-material-data-table": "^0.10.9",
"angular-material-icons": "^0.7.1",
- "angular-messages": "^1.5.8",
- "angular-route": "^1.5.8",
- "angular-sanitize": "^1.5.8",
+ "angular-messages": "1.5.8",
+ "angular-route": "1.5.8",
+ "angular-sanitize": "1.5.8",
"angular-storage": "0.0.15",
- "angular-touch": "^1.5.8",
+ "angular-touch": "1.5.8",
"angular-translate": "^2.12.1",
"angular-translate-handler-log": "^2.12.1",
"angular-translate-interpolation-messageformat": "^2.12.1",
@@ -65,6 +66,7 @@
"react": "^15.4.1",
"react-ace": "^4.1.0",
"react-dom": "^15.4.1",
+ "react-dropzone": "^3.7.3",
"react-schema-form": "^0.3.1",
"react-tap-event-plugin": "^2.0.1",
"reactcss": "^1.0.9",
ui/pom.xml 2(+1 -1)
diff --git a/ui/pom.xml b/ui/pom.xml
index bc68c72..1559ecb 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.0</version>
+ <version>1.0.1</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/src/app/app.js 2(+2 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 3d55ac1..3aa6cea 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -42,6 +42,7 @@ import 'react-dom';
import 'material-ui';
import 'react-schema-form';
import react from 'ngreact';
+import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import thingsboardLogin from './login';
import thingsboardDialogs from './components/datakey-config-dialog.controller';
@@ -88,6 +89,7 @@ angular.module('thingsboard', [
'angular-carousel',
'ngclipboard',
react.name,
+ 'flow',
thingsboardLogin,
thingsboardDialogs,
thingsboardMenu,
ui/src/app/app.run.js 3(+3 -0)
diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js
index 934f021..3f2e7d7 100644
--- a/ui/src/app/app.run.js
+++ b/ui/src/app/app.run.js
@@ -14,9 +14,12 @@
* limitations under the License.
*/
+import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+
/*@ngInject*/
export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
+ $window.Flow = Flow;
var frame = $window.frameElement;
var unauthorizedDialog = null;
var forbiddenDialog = null;
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index ce7668f..e5a1b02 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -51,6 +51,7 @@ function Dashboard() {
widgets: '=',
deviceAliasList: '=',
columns: '=',
+ margins: '=',
isEdit: '=',
isMobile: '=',
isMobileDisabled: '=?',
@@ -61,7 +62,8 @@ function Dashboard() {
onWidgetClicked: '&?',
loadWidgets: '&?',
onInit: '&?',
- onInitFailed: '&?'
+ onInitFailed: '&?',
+ dashboardStyle: '=?'
},
controller: DashboardController,
controllerAs: 'vm',
@@ -108,7 +110,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
},
isMobile: vm.isMobileDisabled ? false : vm.isMobile,
mobileBreakPoint: vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960),
- margins: [10, 10],
+ margins: vm.margins ? vm.margins : [10, 10],
saveGridItemCalculatedHeightInMobile: true
};
@@ -161,6 +163,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.gridsterOpts.columns = vm.columns ? vm.columns : 24;
});
+ $scope.$watch('vm.margins', function () {
+ vm.gridsterOpts.margins = vm.margins ? vm.margins : [10, 10];
+ });
+
$scope.$watch('vm.isEdit', function () {
vm.gridsterOpts.resizable.enabled = vm.isEdit;
vm.gridsterOpts.draggable.enabled = vm.isEdit;
ui/src/app/components/dashboard.tpl.html 112(+57 -55)
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 1127cb0..f0cecef 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -20,61 +20,63 @@
<md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
</md-content>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
- <div id="gridster-child" gridster="vm.gridsterOpts">
- <ul>
-<!-- ng-click="widgetClicked($event, widget)" -->
- <li gridster-item="widget" ng-repeat="widget in vm.widgets">
- <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
- ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
- tb-mousedown="vm.widgetMouseDown($event, widget)"
- tb-mousemove="vm.widgetMouseMove($event, widget)"
- tb-mouseup="vm.widgetMouseUp($event, widget)"
- style="
- cursor: pointer;
- color: {{vm.widgetColor(widget)}};
- background-color: {{vm.widgetBackgroundColor(widget)}};
- padding: {{vm.widgetPadding(widget)}}
- ">
- <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
- <span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
- <tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
- </div>
- <div class="tb-widget-actions" layout="row" layout-align="start center">
- <md-button id="expand-button"
- aria-label="{{ 'fullscreen.fullscreen' | translate }}"
- class="md-icon-button md-primary"></md-button>
- <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
- ng-disabled="vm.loading()"
- class="md-icon-button md-primary"
- ng-click="vm.editWidget($event, widget)"
- aria-label="{{ 'widget.edit' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'widget.edit' | translate }}
- </md-tooltip>
- <md-icon class="material-icons">
- edit
- </md-icon>
- </md-button>
- <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
- ng-disabled="vm.loading()"
- class="md-icon-button md-primary"
- ng-click="vm.removeWidget($event, widget)"
- aria-label="{{ 'widget.remove' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'widget.remove' | translate }}
- </md-tooltip>
- <md-icon class="material-icons">
- close
- </md-icon>
- </md-button>
- </div>
- <div flex layout="column" class="tb-widget-content">
- <div flex tb-widget
- locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+ <div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
+ <div id="gridster-child" gridster="vm.gridsterOpts">
+ <ul>
+ <!-- ng-click="widgetClicked($event, widget)" -->
+ <li gridster-item="widget" ng-repeat="widget in vm.widgets">
+ <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
+ ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
+ tb-mousedown="vm.widgetMouseDown($event, widget)"
+ tb-mousemove="vm.widgetMouseMove($event, widget)"
+ tb-mouseup="vm.widgetMouseUp($event, widget)"
+ style="
+ cursor: pointer;
+ color: {{vm.widgetColor(widget)}};
+ background-color: {{vm.widgetBackgroundColor(widget)}};
+ padding: {{vm.widgetPadding(widget)}}
+ ">
+ <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
+ <span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
+ <tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
- </div>
- </div>
- </li>
- </ul>
+ <div class="tb-widget-actions" layout="row" layout-align="start center">
+ <md-button id="expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button md-primary"></md-button>
+ <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
+ ng-disabled="vm.loading()"
+ class="md-icon-button md-primary"
+ ng-click="vm.editWidget($event, widget)"
+ aria-label="{{ 'widget.edit' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.edit' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">
+ edit
+ </md-icon>
+ </md-button>
+ <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
+ ng-disabled="vm.loading()"
+ class="md-icon-button md-primary"
+ ng-click="vm.removeWidget($event, widget)"
+ aria-label="{{ 'widget.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.remove' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div flex layout="column" class="tb-widget-content">
+ <div flex tb-widget
+ locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
</div>
</md-content>
\ No newline at end of file
ui/src/app/components/react/json-form-image.jsx 105(+105 -0)
diff --git a/ui/src/app/components/react/json-form-image.jsx b/ui/src/app/components/react/json-form-image.jsx
new file mode 100644
index 0000000..2da3edc
--- /dev/null
+++ b/ui/src/app/components/react/json-form-image.jsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.
+ */
+
+import './json-form-image.scss';
+
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import Dropzone from 'react-dropzone';
+import IconButton from 'material-ui/IconButton';
+
+class ThingsboardImage extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onValueChanged = this.onValueChanged.bind(this);
+ this.onDrop = this.onDrop.bind(this);
+ this.onClear = this.onClear.bind(this);
+ var value = props.value ? props.value + '' : null;
+ this.state = {
+ imageUrl: value
+ };
+ }
+
+ onValueChanged(value) {
+ this.setState({
+ imageUrl: value
+ });
+ this.props.onChangeValidate({
+ target: {
+ value: value
+ }
+ });
+ }
+
+ onDrop(files) {
+ var reader = new FileReader();
+ reader.onload = (function(tImg) {
+ return function(event) {
+ tImg.onValueChanged(event.target.result);
+ };
+ })(this);
+ reader.readAsDataURL(files[0]);
+ }
+
+ onClear(event) {
+ if (event) {
+ event.stopPropagation();
+ }
+ this.onValueChanged(null);
+ }
+
+ render() {
+
+ var labelClass = "tb-label";
+ if (this.props.form.required) {
+ labelClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ labelClass += " tb-readonly";
+ }
+ if (this.state.focused) {
+ labelClass += " tb-focused";
+ }
+
+ var previewComponent;
+ if (this.state.imageUrl) {
+ previewComponent = <img className="tb-image-preview" src={this.state.imageUrl} />;
+ } else {
+ previewComponent = <div>No image selected</div>;
+ }
+
+ return (
+ <div className="tb-container">
+ <label className={labelClass}>{this.props.form.title}</label>
+ <div className="tb-image-select-container">
+ <div className="tb-image-preview-container">{previewComponent}</div>
+ <div className="tb-image-clear-container">
+ <IconButton className="tb-image-clear-btn" iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
+ </div>
+ <Dropzone className="tb-dropzone"
+ onDrop={this.onDrop}
+ multiple={false}
+ accept="image/*">
+ <div>Drop an image or click to select a file to upload.</div>
+ </Dropzone>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardImage);
\ No newline at end of file
diff --git a/ui/src/app/components/react/json-form-image.scss b/ui/src/app/components/react/json-form-image.scss
new file mode 100644
index 0000000..58b5eaa
--- /dev/null
+++ b/ui/src/app/components/react/json-form-image.scss
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.
+ */
+
+$previewSize: 100px;
+
+.tb-image-select-container {
+ position: relative;
+ height: $previewSize;
+ width: 100%;
+}
+
+.tb-image-preview {
+ max-width: $previewSize;
+ max-height: $previewSize;
+ width: 100%;
+ height: 100%;
+}
+
+.tb-image-preview-container {
+ position: relative;
+ width: $previewSize;
+ height: $previewSize;
+ margin-right: 12px;
+ border: solid 1px;
+ vertical-align: top;
+ float: left;
+ div {
+ width: 100%;
+ font-size: 18px;
+ text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-dropzone {
+ position: relative;
+ border: dashed 2px;
+ height: $previewSize;
+ vertical-align: top;
+ padding: 0 8px;
+ overflow: hidden;
+ div {
+ width: 100%;
+ font-size: 24px;
+ text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-image-clear-container {
+ width: 48px;
+ height: $previewSize;
+ position: relative;
+ float: right;
+}
+.tb-image-clear-btn {
+ position: absolute !important;
+ top: 50%;
+ transform: translate(0%,-50%) !important;
+}
diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx
index 3e1c046..c641a0a 100644
--- a/ui/src/app/components/react/json-form-schema-form.jsx
+++ b/ui/src/app/components/react/json-form-schema-form.jsx
@@ -26,6 +26,7 @@ import ThingsboardText from './json-form-text.jsx';
import Select from 'react-schema-form/lib/Select';
import Radios from 'react-schema-form/lib/Radios';
import ThingsboardDate from './json-form-date.jsx';
+import ThingsboardImage from './json-form-image.jsx';
import ThingsboardCheckbox from './json-form-checkbox.jsx';
import Help from 'react-schema-form/lib/Help';
import ThingsboardFieldSet from './json-form-fieldset.jsx';
@@ -45,6 +46,7 @@ class ThingsboardSchemaForm extends React.Component {
'select': Select,
'radios': Radios,
'date': ThingsboardDate,
+ 'image': ThingsboardImage,
'checkbox': ThingsboardCheckbox,
'help': Help,
'array': ThingsboardArray,
ui/src/app/dashboard/dashboard.controller.js 29(+29 -0)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 18c2b5f..68978c3 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -16,6 +16,7 @@
/* eslint-disable import/no-unresolved, import/default */
import deviceAliasesTemplate from './device-aliases.tpl.html';
+import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
import addWidgetTemplate from './add-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@@ -55,6 +56,7 @@ export default function DashboardController(types, widgetService, userService,
vm.onAddWidgetClosed = onAddWidgetClosed;
vm.onEditWidgetClosed = onEditWidgetClosed;
vm.openDeviceAliases = openDeviceAliases;
+ vm.openDashboardSettings = openDashboardSettings;
vm.removeWidget = removeWidget;
vm.saveDashboard = saveDashboard;
vm.saveWidget = saveWidget;
@@ -252,6 +254,24 @@ export default function DashboardController(types, widgetService, userService,
});
}
+ function openDashboardSettings($event) {
+ $mdDialog.show({
+ controller: 'DashboardSettingsController',
+ controllerAs: 'vm',
+ templateUrl: dashboardBackgroundTemplate,
+ locals: {
+ gridSettings: angular.copy(vm.dashboard.configuration.gridSettings)
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (gridSettings) {
+ vm.dashboard.configuration.gridSettings = gridSettings;
+ }, function () {
+ });
+ }
+
function editWidget($event, widget) {
$event.stopPropagation();
var newEditingIndex = vm.widgets.indexOf(widget);
@@ -368,6 +388,15 @@ export default function DashboardController(types, widgetService, userService,
w.triggerHandler('resize');
}
}).then(function (widget) {
+ var columns = 24;
+ if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
+ columns = vm.dashboard.configuration.gridSettings.columns;
+ }
+ if (columns != 24) {
+ var ratio = columns / 24;
+ widget.sizeX *= ratio;
+ widget.sizeY *= ratio;
+ }
vm.widgets.push(widget);
}, function () {
});
ui/src/app/dashboard/dashboard.tpl.html 14(+13 -1)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index b18c392..0c815a6 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -59,10 +59,22 @@
<md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDeviceAliases($event)">
{{ 'device.aliases' | translate }}
</md-button>
+ <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDashboardSettings($event)">
+ {{ 'dashboard.settings' | translate }}
+ </md-button>
</section>
- <div class="tb-absolute-fill" ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
+ <div class="tb-absolute-fill"
+ ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
<tb-dashboard
+ dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
+ 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
+ 'background-repeat': 'no-repeat',
+ 'background-attachment': 'scroll',
+ 'background-size': '100%',
+ 'background-position': '0% 0%'}"
widgets="vm.widgets"
+ columns="vm.dashboard.configuration.gridSettings.columns"
+ margins="vm.dashboard.configuration.gridSettings.margins"
device-alias-list="vm.dashboard.configuration.deviceAliases"
is-edit="vm.isEdit || vm.widgetEditMode"
is-mobile="vm.forceDashboardMobileMode"
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
new file mode 100644
index 0000000..d15359e
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.
+ */
+
+import './dashboard-settings.scss';
+
+/*@ngInject*/
+export default function DashboardSettingsController($scope, $mdDialog, gridSettings) {
+
+ var vm = this;
+
+ vm.cancel = cancel;
+ vm.save = save;
+ vm.imageAdded = imageAdded;
+ vm.clearImage = clearImage;
+
+ vm.gridSettings = gridSettings || {};
+
+ vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
+ vm.gridSettings.columns = vm.gridSettings.columns || 24;
+ vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
+ vm.hMargin = vm.gridSettings.margins[0];
+ vm.vMargin = vm.gridSettings.margins[1];
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function imageAdded($file) {
+ var reader = new FileReader();
+ reader.onload = function(event) {
+ $scope.$apply(function() {
+ if (event.target.result && event.target.result.startsWith('data:image/')) {
+ $scope.theForm.$setDirty();
+ vm.gridSettings.backgroundImageUrl = event.target.result;
+ }
+ });
+ };
+ reader.readAsDataURL($file.file);
+ }
+
+ function clearImage() {
+ $scope.theForm.$setDirty();
+ vm.gridSettings.backgroundImageUrl = null;
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
+ $mdDialog.hide(vm.gridSettings);
+ }
+}
ui/src/app/dashboard/dashboard-settings.scss 91(+91 -0)
diff --git a/ui/src/app/dashboard/dashboard-settings.scss b/ui/src/app/dashboard/dashboard-settings.scss
new file mode 100644
index 0000000..2231372
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.scss
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * 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.
+ */
+$previewSize: 100px;
+
+.file-input {
+ display: none;
+}
+
+.tb-container {
+ position: relative;
+ margin-top: 32px;
+ padding: 10px 0;
+}
+
+.tb-image-select-container {
+ position: relative;
+ height: $previewSize;
+ width: 100%;
+}
+
+.tb-image-preview {
+ max-width: $previewSize;
+ max-height: $previewSize;
+ width: auto;
+ height: auto;
+}
+
+.tb-image-preview-container {
+ position: relative;
+ width: $previewSize;
+ height: $previewSize;
+ margin-right: 12px;
+ border: solid 1px;
+ vertical-align: top;
+ float: left;
+ div {
+ width: 100%;
+ font-size: 18px;
+ text-align: center;
+ }
+ div, .tb-image-preview {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-flow-drop {
+ position: relative;
+ border: dashed 2px;
+ height: $previewSize;
+ vertical-align: top;
+ padding: 0 8px;
+ overflow: hidden;
+ min-width: 300px;
+ label {
+ width: 100%;
+ font-size: 24px;
+ text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-image-clear-container {
+ width: 48px;
+ height: $previewSize;
+ position: relative;
+ float: right;
+}
+.tb-image-clear-btn {
+ position: absolute !important;
+ top: 50%;
+ transform: translate(0%,-50%) !important;
+}
ui/src/app/dashboard/dashboard-settings.tpl.html 115(+115 -0)
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
new file mode 100644
index 0000000..f69eb02
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -0,0 +1,115 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ 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.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.settings' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>dashboard.settings</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>dashboard.columns-count</label>
+ <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
+ max="1000" />
+ <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.columns-count-required</div>
+ <div ng-message="min" translate>dashboard.min-columns-count-message</div>
+ <div ng-message="max">dashboard.max-columns-count-message</div>
+ </div>
+ </md-input-container>
+ <small translate>dashboard.widgets-margins</small>
+ <div flex layout="row">
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.horizontal-margin</label>
+ <input required type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.vertical-margin</label>
+ <input required type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.vertical-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+ </div>
+ </md-input-container>
+ </div>
+ <div flex
+ ng-required="false"
+ md-color-picker
+ ng-model="vm.gridSettings.backgroundColor"
+ label="{{ 'dashboard.background-color' | translate }}"
+ icon="format_color_fill"
+ default="rgba(0,0,0,0)"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ <div class="tb-container">
+ <label class="tb-label" translate>dashboard.background-image</label>
+ <div flow-init="{singleFile:true}"
+ flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
+ <div class="tb-image-preview-container">
+ <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
+ <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
+ </div>
+ <div class="tb-image-clear-container">
+ <md-button ng-click="vm.clearImage()"
+ class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="select" translate>dashboard.drop-image</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+ </div>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/dashboard/index.js 2(+2 -0)
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index d740b10..7400463 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -34,6 +34,7 @@ import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller';
import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.controller';
+import DashboardSettingsController from './dashboard-settings.controller';
import AssignDashboardToCustomerController from './assign-to-customer.controller';
import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
import AddWidgetController from './add-widget.controller';
@@ -59,6 +60,7 @@ export default angular.module('thingsboard.dashboard', [
.controller('DashboardsController', DashboardsController)
.controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController)
+ .controller('DashboardSettingsController', DashboardSettingsController)
.controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
.controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
.controller('AddWidgetController', AddWidgetController)
ui/src/app/device/device.directive.js 13(+11 -2)
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index 918040b..7f84016 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -20,18 +20,23 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function DeviceDirective($compile, $templateCache, toast, $translate, types, customerService) {
+export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
var linker = function (scope, element) {
var template = $templateCache.get(deviceFieldsetTemplate);
element.html(template);
scope.isAssignedToCustomer = false;
-
scope.assignedCustomer = null;
+ scope.deviceCredentials = null;
scope.$watch('device', function(newVal) {
if (newVal) {
+ deviceService.getDeviceCredentials(scope.device.id.id).then(
+ function success(credentials) {
+ scope.deviceCredentials = credentials;
+ }
+ );
if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getCustomer(scope.device.customerId.id).then(
@@ -50,6 +55,10 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
+ scope.onAccessTokenCopied = function() {
+ toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ };
+
$compile(element.contents())(scope);
}
return {
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index 6d9892e..3bd8f4f 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -36,6 +36,13 @@
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyId</span>
</md-button>
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onAccessTokenCopied(e)"
+ data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>device.copyAccessToken</span>
+ </md-button>
</div>
<md-content class="md-padding" layout="column">
ui/src/locale/en_US.json 23(+22 -1)
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 4a5e48d..9da68d6 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -192,7 +192,26 @@
"select-existing": "Select existing dashboard",
"create-new": "Create new dashboard",
"new-dashboard-title": "New dashboard title",
- "open-dashboard": "Open dashboard"
+ "open-dashboard": "Open dashboard",
+ "set-background": "Set background",
+ "background-color": "Background color",
+ "background-image": "Background image",
+ "no-image": "No image selected",
+ "drop-image": "Drop an image or click to select a file to upload.",
+ "settings": "Settings",
+ "columns-count": "Columns count",
+ "columns-count-required": "Columns count is required.",
+ "min-columns-count-message": "Only 10 minimum column count is allowed.",
+ "max-columns-count-message": "Only 1000 maximum column count is allowed.",
+ "widgets-margins": "Margin between widgets",
+ "horizontal-margin": "Horizontal margin",
+ "horizontal-margin-required": "Horizontal margin value is required.",
+ "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.",
+ "max-horizontal-margin-message": "Only 50 is allowed as maximum horizontal margin value.",
+ "vertical-margin": "Vertical margin",
+ "vertical-margin-required": "Vertical margin value is required.",
+ "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
+ "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value."
},
"datakey": {
"settings": "Settings",
@@ -280,7 +299,9 @@
"events": "Events",
"details": "Details",
"copyId": "Copy device Id",
+ "copyAccessToken": "Copy access token",
"idCopiedMessage": "Device Id has been copied to clipboard",
+ "accessTokenCopiedMessage": "Device access token has been copied to clipboard",
"assignedToCustomer": "Assigned to customer",
"unable-delete-device-alias-title": "Unable to delete device alias",
"unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}"