Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard

pull/1635/head
nordmif 8 years ago
parent
commit
f2ba1aaa3c
  1. 16
      application/src/main/data/json/system/widget_bundles/maps.json
  2. 22
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 68
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  4. 13
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  5. 2
      application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
  6. 46
      application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java
  7. 128
      application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
  8. 2
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  9. 4
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  10. 2
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
  13. 31
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  14. 1
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
  15. 8
      application/src/main/resources/thingsboard.yml
  16. 4
      application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
  17. 2
      application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java
  18. 14
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
  19. 2
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
  20. 4
      dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
  21. 2
      dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
  22. 2
      dao/src/main/java/org/thingsboard/server/dao/audit/sink/DummyAuditLogSink.java
  23. 16
      dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java
  24. 43
      dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
  25. 2
      dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java
  26. 2
      dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java
  27. 2
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
  28. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  29. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  30. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java
  31. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
  32. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java
  33. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
  34. 1
      dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
  35. 8
      dao/src/test/resources/application-test.properties
  36. 1
      dao/src/test/resources/sql-test.properties
  37. 48
      pom.xml
  38. 3
      ui/src/app/api/widget.service.js
  39. 8
      ui/src/app/widget/lib/map-widget2.js
  40. 702
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js
  41. 106
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss
  42. 54
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html

16
application/src/main/data/json/system/widget_bundles/maps.json

File diff suppressed because one or more lines are too long

22
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -118,17 +118,23 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
this.rpcSubscriptions = new HashMap<>(); this.rpcSubscriptions = new HashMap<>();
this.toDeviceRpcPendingMap = new HashMap<>(); this.toDeviceRpcPendingMap = new HashMap<>();
this.toServerRpcPendingMap = new HashMap<>(); this.toServerRpcPendingMap = new HashMap<>();
initAttributes(); if (initAttributes()) {
restoreSessions(); restoreSessions();
}
} }
private void initAttributes() { private boolean initAttributes() {
Device device = systemContext.getDeviceService().findDeviceById(tenantId, deviceId); Device device = systemContext.getDeviceService().findDeviceById(tenantId, deviceId);
this.deviceName = device.getName(); if (device != null) {
this.deviceType = device.getType(); this.deviceName = device.getName();
this.defaultMetaData = new TbMsgMetaData(); this.deviceType = device.getType();
this.defaultMetaData.putValue("deviceName", deviceName); this.defaultMetaData = new TbMsgMetaData();
this.defaultMetaData.putValue("deviceType", deviceType); this.defaultMetaData.putValue("deviceName", deviceName);
this.defaultMetaData.putValue("deviceType", deviceType);
return true;
} else {
return false;
}
} }
void processRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg) { void processRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg) {

68
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -91,17 +91,19 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
public void start(ActorContext context) { public void start(ActorContext context) {
if (!started) { if (!started) {
RuleChain ruleChain = service.findRuleChainById(tenantId, entityId); RuleChain ruleChain = service.findRuleChainById(tenantId, entityId);
ruleChainName = ruleChain.getName(); if (ruleChain != null) {
List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId); ruleChainName = ruleChain.getName();
log.trace("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
// Creating and starting the actors; log.trace("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
for (RuleNode ruleNode : ruleNodeList) { // Creating and starting the actors;
log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode); for (RuleNode ruleNode : ruleNodeList) {
ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode); log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode)); ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
}
initRoutes(ruleChain, ruleNodeList);
started = true;
} }
initRoutes(ruleChain, ruleNodeList);
started = true;
} else { } else {
onUpdate(context); onUpdate(context);
} }
@ -110,31 +112,33 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
@Override @Override
public void onUpdate(ActorContext context) { public void onUpdate(ActorContext context) {
RuleChain ruleChain = service.findRuleChainById(tenantId, entityId); RuleChain ruleChain = service.findRuleChainById(tenantId, entityId);
ruleChainName = ruleChain.getName(); if (ruleChain != null) {
List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId); ruleChainName = ruleChain.getName();
log.trace("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
for (RuleNode ruleNode : ruleNodeList) { log.trace("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
RuleNodeCtx existing = nodeActors.get(ruleNode.getId()); for (RuleNode ruleNode : ruleNodeList) {
if (existing == null) { RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode); if (existing == null) {
ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode); log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode)); ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
} else { nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
log.trace("[{}][{}] Updating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode); } else {
existing.setSelf(ruleNode); log.trace("[{}][{}] Updating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self); existing.setSelf(ruleNode);
existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self);
}
} }
}
Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet()); Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList()); List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList());
removedRules.forEach(ruleNodeId -> { removedRules.forEach(ruleNodeId -> {
log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId); log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId);
RuleNodeCtx removed = nodeActors.remove(ruleNodeId); RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self); removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self);
}); });
initRoutes(ruleChain, ruleNodeList); initRoutes(ruleChain, ruleNodeList);
}
} }
@Override @Override

13
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -55,7 +55,9 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void start(ActorContext context) throws Exception { public void start(ActorContext context) throws Exception {
tbNode = initComponent(ruleNode); tbNode = initComponent(ruleNode);
state = ComponentLifecycleState.ACTIVE; if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE;
}
} }
@Override @Override
@ -118,9 +120,12 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
private TbNode initComponent(RuleNode ruleNode) throws Exception { private TbNode initComponent(RuleNode ruleNode) throws Exception {
Class<?> componentClazz = Class.forName(ruleNode.getType()); TbNode tbNode = null;
TbNode tbNode = (TbNode) (componentClazz.newInstance()); if (ruleNode != null) {
tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration())); Class<?> componentClazz = Class.forName(ruleNode.getType());
tbNode = (TbNode) (componentClazz.newInstance());
tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
}
return tbNode; return tbNode;
} }

2
application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java

@ -22,7 +22,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Configuration @Configuration
@ConfigurationProperties(prefix = "audit_log.logging_level") @ConfigurationProperties(prefix = "audit-log.logging-level")
public class AuditLogLevelProperties { public class AuditLogLevelProperties {
private Map<String, String> mask = new HashMap<>(); private Map<String, String> mask = new HashMap<>();

46
application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2019 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.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Configuration
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskScheduler());
}
@Bean(destroyMethod="shutdown")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler threadPoolScheduler = new ThreadPoolTaskScheduler();
threadPoolScheduler.setThreadNamePrefix("TB-Scheduling-");
threadPoolScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
threadPoolScheduler.setRemoveOnCancelPolicy(true);
return threadPoolScheduler;
}
}

128
application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java

@ -15,11 +15,28 @@
*/ */
package org.thingsboard.server.config; package org.thingsboard.server.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.ExtendedProperties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
@Configuration @Configuration
public class ThingsboardMessageConfiguration { public class ThingsboardMessageConfiguration {
@ -32,5 +49,114 @@ public class ThingsboardMessageConfiguration {
messageSource.setDefaultEncoding("UTF-8"); messageSource.setDefaultEncoding("UTF-8");
return messageSource; return messageSource;
} }
private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:/templates/";
private ResourceLoader resourceLoader = new DefaultResourceLoader();
@Bean
public VelocityEngine velocityEngine() {
VelocityEngine velocityEngine = new VelocityEngine();
try {
Resource resource = resourceLoader.getResource(DEFAULT_RESOURCE_LOADER_PATH);
File file = resource.getFile();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file");
velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true");
velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, file.getAbsolutePath());
} catch (IOException e) {
initSpringResourceLoader(velocityEngine, DEFAULT_RESOURCE_LOADER_PATH);
}
velocityEngine.init();
return velocityEngine;
}
private void initSpringResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
velocityEngine.setProperty(
RuntimeConstants.RESOURCE_LOADER, SpringResourceLoader.NAME);
velocityEngine.setProperty(
SpringResourceLoader.SPRING_RESOURCE_LOADER_CLASS, SpringResourceLoader.class.getName());
velocityEngine.setProperty(
SpringResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true");
velocityEngine.setApplicationAttribute(
SpringResourceLoader.SPRING_RESOURCE_LOADER, resourceLoader);
velocityEngine.setApplicationAttribute(
SpringResourceLoader.SPRING_RESOURCE_LOADER_PATH, resourceLoaderPath);
}
@Slf4j
static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader {
public static final String NAME = "spring";
public static final String SPRING_RESOURCE_LOADER_CLASS = "spring.resource.loader.class";
public static final String SPRING_RESOURCE_LOADER_CACHE = "spring.resource.loader.cache";
public static final String SPRING_RESOURCE_LOADER = "spring.resource.loader";
public static final String SPRING_RESOURCE_LOADER_PATH = "spring.resource.loader.path";
private org.springframework.core.io.ResourceLoader resourceLoader;
private String[] resourceLoaderPaths;
@Override
public void init(ExtendedProperties configuration) {
this.resourceLoader = (org.springframework.core.io.ResourceLoader)
this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER);
String resourceLoaderPath = (String) this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER_PATH);
if (this.resourceLoader == null) {
throw new IllegalArgumentException(
"'resourceLoader' application attribute must be present for SpringResourceLoader");
}
if (resourceLoaderPath == null) {
throw new IllegalArgumentException(
"'resourceLoaderPath' application attribute must be present for SpringResourceLoader");
}
this.resourceLoaderPaths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
for (int i = 0; i < this.resourceLoaderPaths.length; i++) {
String path = this.resourceLoaderPaths[i];
if (!path.endsWith("/")) {
this.resourceLoaderPaths[i] = path + "/";
}
}
if (log.isInfoEnabled()) {
log.info("SpringResourceLoader for Velocity: using resource loader [" + this.resourceLoader +
"] and resource loader paths " + Arrays.asList(this.resourceLoaderPaths));
}
}
@Override
public InputStream getResourceStream(String source) throws ResourceNotFoundException {
if (log.isDebugEnabled()) {
log.debug("Looking for Velocity resource with name [" + source + "]");
}
for (String resourceLoaderPath : this.resourceLoaderPaths) {
org.springframework.core.io.Resource resource =
this.resourceLoader.getResource(resourceLoaderPath + source);
try {
return resource.getInputStream();
}
catch (IOException ex) {
if (log.isDebugEnabled()) {
log.debug("Could not find Velocity resource: " + resource);
}
}
}
throw new ResourceNotFoundException(
"Could not find resource [" + source + "] in Spring resource loader path");
}
@Override
public boolean isSourceModified(org.apache.velocity.runtime.resource.Resource resource) {
return false;
}
@Override
public long getLastModified(org.apache.velocity.runtime.resource.Resource resource) {
return 0;
}
}
} }

2
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -57,7 +57,7 @@ import java.util.List;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true) @EnableGlobalMethodSecurity(prePostEnabled=true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) @Order(SecurityProperties.BASIC_AUTH_ORDER)
public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapter { public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";

4
application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java

@ -58,7 +58,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override @Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception { Map<String, Object> attributes) throws Exception {
SecurityUser user = null; SecurityUser user = null;
try { try {
user = getCurrentUser(); user = getCurrentUser();
@ -73,7 +73,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override @Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) { Exception exception) {
//Do nothing //Do nothing
} }
}); });

2
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -392,4 +392,4 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
} }
} }
} }

2
application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.thingsboard.server.dao.cassandra.CassandraInstallCluster; import org.thingsboard.server.dao.cassandra.CassandraInstallCluster;
import org.thingsboard.server.service.install.cql.CQLStatementsParser; import org.thingsboard.server.service.install.cql.CQLStatementsParser;
@ -30,6 +31,7 @@ public abstract class CassandraAbstractDatabaseSchemaService implements Database
private static final String CASSANDRA_DIR = "cassandra"; private static final String CASSANDRA_DIR = "cassandra";
@Autowired @Autowired
@Qualifier("CassandraInstallCluster")
private CassandraInstallCluster cluster; private CassandraInstallCluster cluster;
@Autowired @Autowired

2
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.install;
import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.KeyspaceMetadata;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.cassandra.CassandraCluster;
@ -65,6 +66,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
private CassandraCluster cluster; private CassandraCluster cluster;
@Autowired @Autowired
@Qualifier("CassandraInstallCluster")
private CassandraInstallCluster installCluster; private CassandraInstallCluster installCluster;
@Autowired @Autowired

31
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -18,7 +18,9 @@ package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.VelocityException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
@ -26,7 +28,6 @@ import org.springframework.core.NestedRuntimeException;
import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.velocity.VelocityEngineUtils;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -39,6 +40,8 @@ import org.thingsboard.server.dao.settings.AdminSettingsService;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -126,7 +129,7 @@ public class DefaultMailService implements MailService {
Map<String, Object> model = new HashMap<String, Object>(); Map<String, Object> model = new HashMap<String, Object>();
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine, String message = mergeTemplateIntoString(this.engine,
"test.vm", UTF_8, model); "test.vm", UTF_8, model);
sendMail(testMailSender, mailFrom, email, subject, message); sendMail(testMailSender, mailFrom, email, subject, message);
@ -141,7 +144,7 @@ public class DefaultMailService implements MailService {
model.put("activationLink", activationLink); model.put("activationLink", activationLink);
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine, String message = mergeTemplateIntoString(this.engine,
"activation.vm", UTF_8, model); "activation.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message); sendMail(mailSender, mailFrom, email, subject, message);
@ -156,7 +159,7 @@ public class DefaultMailService implements MailService {
model.put("loginLink", loginLink); model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine, String message = mergeTemplateIntoString(this.engine,
"account.activated.vm", UTF_8, model); "account.activated.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message); sendMail(mailSender, mailFrom, email, subject, message);
@ -171,7 +174,7 @@ public class DefaultMailService implements MailService {
model.put("passwordResetLink", passwordResetLink); model.put("passwordResetLink", passwordResetLink);
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine, String message = mergeTemplateIntoString(this.engine,
"reset.password.vm", UTF_8, model); "reset.password.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message); sendMail(mailSender, mailFrom, email, subject, message);
@ -186,7 +189,7 @@ public class DefaultMailService implements MailService {
model.put("loginLink", loginLink); model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine, String message = mergeTemplateIntoString(this.engine,
"password.was.reset.vm", UTF_8, model); "password.was.reset.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message); sendMail(mailSender, mailFrom, email, subject, message);
@ -225,6 +228,22 @@ public class DefaultMailService implements MailService {
} }
} }
private static String mergeTemplateIntoString(VelocityEngine velocityEngine, String templateLocation,
String encoding, Map<String, Object> model) throws VelocityException {
StringWriter result = new StringWriter();
mergeTemplate(velocityEngine, templateLocation, encoding, model, result);
return result.toString();
}
private static void mergeTemplate(
VelocityEngine velocityEngine, String templateLocation, String encoding,
Map<String, Object> model, Writer writer) throws VelocityException {
VelocityContext velocityContext = new VelocityContext(model);
velocityEngine.mergeTemplate(templateLocation, encoding, velocityContext, writer);
}
protected ThingsboardException handleException(Exception exception) { protected ThingsboardException handleException(Exception exception) {
String message; String message;
if (exception instanceof NestedRuntimeException) { if (exception instanceof NestedRuntimeException) {

1
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java

@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;

8
application/src/main/resources/thingsboard.yml

@ -293,11 +293,13 @@ spring.mvc.cors:
# spring serve gzip compressed static resources # spring serve gzip compressed static resources
spring.resources.chain: spring.resources.chain:
gzipped: "true" compressed: "true"
strategy: strategy:
content: content:
enabled: "true" enabled: "true"
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true"
# HSQLDB DAO Configuration # HSQLDB DAO Configuration
spring: spring:
data: data:
@ -331,7 +333,7 @@ spring:
# password: "${SPRING_DATASOURCE_PASSWORD:postgres}" # password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
# Audit log parameters # Audit log parameters
audit_log: audit-log:
# Enable/disable audit log functionality. # Enable/disable audit log functionality.
enabled: "${AUDIT_LOG_ENABLED:true}" enabled: "${AUDIT_LOG_ENABLED:true}"
# Specify partitioning size for audit log by tenant id storage. Example MINUTES, HOURS, DAYS, MONTHS # Specify partitioning size for audit log by tenant id storage. Example MINUTES, HOURS, DAYS, MONTHS
@ -340,7 +342,7 @@ audit_log:
default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}" default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}"
# Logging levels per each entity type. # Logging levels per each entity type.
# Allowed values: OFF (disable), W (log write operations), RW (log read and write operations) # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations)
logging_level: logging-level:
mask: mask:
"device": "${AUDIT_LOG_MASK_DEVICE:W}" "device": "${AUDIT_LOG_MASK_DEVICE:W}"
"asset": "${AUDIT_LOG_MASK_ASSET:W}" "asset": "${AUDIT_LOG_MASK_ASSET:W}"

4
application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java

@ -128,7 +128,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
String accessToken = deviceCredentials.getCredentialsId(); String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken); assertNotNull(accessToken);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString(); String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(), doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(),
@ -183,7 +183,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
String accessToken = deviceCredentials.getCredentialsId(); String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken); assertNotNull(accessToken);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString(); String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(), doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(),

2
application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java

@ -111,7 +111,7 @@ public abstract class AbstractMqttTelemetryIntegrationTest extends AbstractContr
client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value()); client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value());
String payload = "{\"key\":\"value\"}"; String payload = "{\"key\":\"value\"}";
String result = doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk()); String result = doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk());
latch.await(3, TimeUnit.SECONDS); latch.await(10, TimeUnit.SECONDS);
assertEquals(payload, callback.getPayload()); assertEquals(payload, callback.getPayload());
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
} }

14
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -18,11 +18,7 @@ package org.thingsboard.server.dao;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.dao.model.ToData; import org.thingsboard.server.dao.model.ToData;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public abstract class DaoUtil { public abstract class DaoUtil {
@ -50,6 +46,14 @@ public abstract class DaoUtil {
return object; return object;
} }
public static <T> T getData(Optional<? extends ToData<T>> data) {
T object = null;
if (data.isPresent()) {
object = data.get().toData();
}
return object;
}
public static UUID getId(UUIDBased idBased) { public static UUID getId(UUIDBased idBased) {
UUID id = null; UUID id = null;
if (idBased != null) { if (idBased != null) {

2
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java

@ -60,7 +60,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Slf4j @Slf4j
@Service @Service
@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true") @ConditionalOnProperty(prefix = "audit-log", value = "enabled", havingValue = "true")
public class AuditLogServiceImpl implements AuditLogService { public class AuditLogServiceImpl implements AuditLogService {
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();

4
dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java

@ -88,11 +88,11 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
protected ExecutorService readResultsProcessingExecutor; protected ExecutorService readResultsProcessingExecutor;
@Value("${audit_log.by_tenant_partitioning}") @Value("${audit-log.by_tenant_partitioning}")
private String partitioning; private String partitioning;
private TsPartitionDate tsFormat; private TsPartitionDate tsFormat;
@Value("${audit_log.default_query_period}") @Value("${audit-log.default_query_period}")
private Integer defaultQueryPeriodInDays; private Integer defaultQueryPeriodInDays;
private PreparedStatement partitionInsertStmt; private PreparedStatement partitionInsertStmt;

2
dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java

@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import java.util.List; import java.util.List;
@Service @Service
@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "false") @ConditionalOnProperty(prefix = "audit-log", value = "enabled", havingValue = "false")
public class DummyAuditLogServiceImpl implements AuditLogService { public class DummyAuditLogServiceImpl implements AuditLogService {
@Override @Override

2
dao/src/main/java/org/thingsboard/server/dao/audit/sink/DummyAuditLogSink.java

@ -20,7 +20,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.audit.AuditLog;
@Component @Component
@ConditionalOnProperty(prefix = "audit_log.sink", value = "type", havingValue = "none") @ConditionalOnProperty(prefix = "audit-log.sink", value = "type", havingValue = "none")
public class DummyAuditLogSink implements AuditLogSink { public class DummyAuditLogSink implements AuditLogSink {
@Override @Override

16
dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java

@ -44,7 +44,7 @@ import java.time.format.DateTimeFormatter;
import java.util.Collections; import java.util.Collections;
@Component @Component
@ConditionalOnProperty(prefix = "audit_log.sink", value = "type", havingValue = "elasticsearch") @ConditionalOnProperty(prefix = "audit-log.sink", value = "type", havingValue = "elasticsearch")
@Slf4j @Slf4j
public class ElasticsearchAuditLogSink implements AuditLogSink { public class ElasticsearchAuditLogSink implements AuditLogSink {
@ -54,19 +54,19 @@ public class ElasticsearchAuditLogSink implements AuditLogSink {
private final ObjectMapper mapper = new ObjectMapper(); private final ObjectMapper mapper = new ObjectMapper();
@Value("${audit_log.sink.index_pattern}") @Value("${audit-log.sink.index_pattern}")
private String indexPattern; private String indexPattern;
@Value("${audit_log.sink.scheme_name}") @Value("${audit-log.sink.scheme_name}")
private String schemeName; private String schemeName;
@Value("${audit_log.sink.host}") @Value("${audit-log.sink.host}")
private String host; private String host;
@Value("${audit_log.sink.port}") @Value("${audit-log.sink.port}")
private int port; private int port;
@Value("${audit_log.sink.user_name}") @Value("${audit-log.sink.user_name}")
private String userName; private String userName;
@Value("${audit_log.sink.password}") @Value("${audit-log.sink.password}")
private String password; private String password;
@Value("${audit_log.sink.date_format}") @Value("${audit-log.sink.date_format}")
private String dateFormat; private String dateFormat;
private RestClient restClient; private RestClient restClient;

43
dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java

@ -19,14 +19,37 @@ import lombok.Data;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
@Configuration @Configuration
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false) @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false)
@ -57,15 +80,12 @@ public class TBRedisCacheConfiguration {
} }
@Bean @Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory cf) { public CacheManager cacheManager(RedisConnectionFactory cf) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>(); DefaultFormattingConversionService redisConversionService = new DefaultFormattingConversionService();
redisTemplate.setConnectionFactory(cf); RedisCacheConfiguration.registerDefaultConverters(redisConversionService);
return redisTemplate; registerDefaultConverters(redisConversionService);
} RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().withConversionService(redisConversionService);
return RedisCacheManager.builder(cf).cacheDefaults(configuration).build();
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
return new RedisCacheManager(redisTemplate);
} }
@Bean @Bean
@ -73,5 +93,8 @@ public class TBRedisCacheConfiguration {
return new PreviousDeviceCredentialsIdKeyGenerator(); return new PreviousDeviceCredentialsIdKeyGenerator();
} }
private static void registerDefaultConverters(ConverterRegistry registry) {
Assert.notNull(registry, "ConverterRegistry must not be null!");
registry.addConverter(EntityId.class, String.class, EntityId::toString);
}
} }

2
dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java

@ -21,7 +21,7 @@ import org.thingsboard.server.dao.util.NoSqlAnyDao;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@Component @Component("CassandraCluster")
@NoSqlAnyDao @NoSqlAnyDao
public class CassandraCluster extends AbstractCassandraCluster { public class CassandraCluster extends AbstractCassandraCluster {

2
dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java

@ -21,7 +21,7 @@ import org.thingsboard.server.dao.util.NoSqlAnyDao;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@Component @Component("CassandraInstallCluster")
@NoSqlAnyDao @NoSqlAnyDao
@Profile("install") @Profile("install")
public class CassandraInstallCluster extends AbstractCassandraCluster { public class CassandraInstallCluster extends AbstractCassandraCluster {

2
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java

@ -27,6 +27,7 @@ import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.exceptions.CodecNotFoundException; import com.datastax.driver.core.exceptions.CodecNotFoundException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.model.type.AuthorityCodec; import org.thingsboard.server.dao.model.type.AuthorityCodec;
@ -44,6 +45,7 @@ import java.util.concurrent.ConcurrentMap;
public abstract class CassandraAbstractDao { public abstract class CassandraAbstractDao {
@Autowired @Autowired
@Qualifier("CassandraCluster")
protected CassandraCluster cluster; protected CassandraCluster cluster;
private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>(); private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();

9
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java

@ -27,6 +27,7 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseEntity;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@ -67,23 +68,23 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
@Override @Override
public D findById(TenantId tenantId, UUID key) { public D findById(TenantId tenantId, UUID key) {
log.debug("Get entity by key {}", key); log.debug("Get entity by key {}", key);
E entity = getCrudRepository().findOne(fromTimeUUID(key)); Optional<E> entity = getCrudRepository().findById(fromTimeUUID(key));
return DaoUtil.getData(entity); return DaoUtil.getData(entity);
} }
@Override @Override
public ListenableFuture<D> findByIdAsync(TenantId tenantId, UUID key) { public ListenableFuture<D> findByIdAsync(TenantId tenantId, UUID key) {
log.debug("Get entity by key async {}", key); log.debug("Get entity by key async {}", key);
return service.submit(() -> DaoUtil.getData(getCrudRepository().findOne(fromTimeUUID(key)))); return service.submit(() -> DaoUtil.getData(getCrudRepository().findById(fromTimeUUID(key))));
} }
@Override @Override
@Transactional @Transactional
public boolean removeById(TenantId tenantId, UUID id) { public boolean removeById(TenantId tenantId, UUID id) {
String key = fromTimeUUID(id); String key = fromTimeUUID(id);
getCrudRepository().delete(key); getCrudRepository().deleteById(key);
log.debug("Remove request: {}", key); log.debug("Remove request: {}", key);
return getCrudRepository().findOne(key) == null; return !getCrudRepository().existsById(key);
} }
@Override @Override

6
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -52,7 +52,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
AttributeKvCompositeKey compositeKey = AttributeKvCompositeKey compositeKey =
getAttributeKvCompositeKey(entityId, attributeType, attributeKey); getAttributeKvCompositeKey(entityId, attributeType, attributeKey);
return Futures.immediateFuture( return Futures.immediateFuture(
Optional.ofNullable(DaoUtil.getData(attributeKvRepository.findOne(compositeKey)))); Optional.ofNullable(DaoUtil.getData(attributeKvRepository.findById(compositeKey))));
} }
@Override @Override
@ -64,7 +64,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
getAttributeKvCompositeKey(entityId, attributeType, attributeKey)) getAttributeKvCompositeKey(entityId, attributeType, attributeKey))
.collect(Collectors.toList()); .collect(Collectors.toList());
return Futures.immediateFuture( return Futures.immediateFuture(
DaoUtil.convertDataList(Lists.newArrayList(attributeKvRepository.findAll(compositeKeys)))); DaoUtil.convertDataList(Lists.newArrayList(attributeKvRepository.findAllById(compositeKeys))));
} }
@Override @Override
@ -103,7 +103,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
}).collect(Collectors.toList()); }).collect(Collectors.toList());
return service.submit(() -> { return service.submit(() -> {
attributeKvRepository.delete(entitiesToDelete); attributeKvRepository.deleteAll(entitiesToDelete);
return null; return null;
}); });
} }

2
dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java

@ -66,7 +66,7 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao<Comp
if (component.getId() == null) { if (component.getId() == null) {
component.setId(new ComponentDescriptorId(UUIDs.timeBased())); component.setId(new ComponentDescriptorId(UUIDs.timeBased()));
} }
if (componentDescriptorRepository.findOne(UUIDConverter.fromTimeUUID(component.getId().getId())) == null) { if (!componentDescriptorRepository.existsById(UUIDConverter.fromTimeUUID(component.getId().getId()))) {
return Optional.of(save(tenantId, component)); return Optional.of(save(tenantId, component));
} }
return Optional.empty(); return Optional.empty();

8
dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java

@ -97,13 +97,13 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
@Override @Override
public ListenableFuture<Boolean> checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { public ListenableFuture<Boolean> checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup); RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
return service.submit(() -> relationRepository.findOne(key) != null); return service.submit(() -> relationRepository.existsById(key));
} }
@Override @Override
public ListenableFuture<EntityRelation> getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { public ListenableFuture<EntityRelation> getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup); RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
return service.submit(() -> DaoUtil.getData(relationRepository.findOne(key))); return service.submit(() -> DaoUtil.getData(relationRepository.findById(key)));
} }
private RelationCompositeKey getRelationCompositeKey(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { private RelationCompositeKey getRelationCompositeKey(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
@ -152,9 +152,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
} }
private boolean deleteRelationIfExists(RelationCompositeKey key) { private boolean deleteRelationIfExists(RelationCompositeKey key) {
boolean relationExistsBeforeDelete = relationRepository.exists(key); boolean relationExistsBeforeDelete = relationRepository.existsById(key);
if (relationExistsBeforeDelete) { if (relationExistsBeforeDelete) {
relationRepository.delete(key); relationRepository.deleteById(key);
} }
return relationExistsBeforeDelete; return relationExistsBeforeDelete;
} }

2
dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java

@ -53,7 +53,7 @@ public interface RelationRepository
RelationEntity save(RelationEntity entity); RelationEntity save(RelationEntity entity);
@Transactional @Transactional
void delete(RelationCompositeKey id); void deleteById(RelationCompositeKey id);
@Transactional @Transactional
void deleteByFromIdAndFromType(String fromId, String fromType); void deleteByFromIdAndFromType(String fromId, String fromType);

6
dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java

@ -284,10 +284,10 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
entityId.getEntityType(), entityId.getEntityType(),
fromTimeUUID(entityId.getId()), fromTimeUUID(entityId.getId()),
key); key);
TsKvLatestEntity entry = tsKvLatestRepository.findOne(compositeKey); Optional<TsKvLatestEntity> entry = tsKvLatestRepository.findById(compositeKey);
TsKvEntry result; TsKvEntry result;
if (entry != null) { if (entry.isPresent()) {
result = DaoUtil.getData(entry); result = DaoUtil.getData(entry.get());
} else { } else {
result = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); result = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
} }

1
dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java

@ -82,6 +82,7 @@ public class CustomCassandraCQLUnit extends BaseCassandraUnit {
session = null; session = null;
cluster = null; cluster = null;
} }
System.setSecurityManager(null);
} }
// Getters for those who do not like to directly access fields // Getters for those who do not like to directly access fields

8
dao/src/test/resources/application-test.properties

@ -4,10 +4,10 @@ zk.zk_dir=/thingsboard
updates.enabled=false updates.enabled=false
audit_log.enabled=true audit-log.enabled=true
audit_log.by_tenant_partitioning=MONTHS audit-log.by_tenant_partitioning=MONTHS
audit_log.default_query_period=30 audit-log.default_query_period=30
audit_log.sink.type=none audit-log.sink.type=none
cache.type=caffeine cache.type=caffeine
#cache.type=redis #cache.type=redis

1
dao/src/test/resources/sql-test.properties

@ -4,6 +4,7 @@ database.entities.type=sql
sql.ts_inserts_executor_type=fixed sql.ts_inserts_executor_type=fixed
sql.ts_inserts_fixed_thread_pool_size=10 sql.ts_inserts_fixed_thread_pool_size=10
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.show-sql=false spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=validate spring.jpa.hibernate.ddl-auto=validate
spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect

48
pom.xml

@ -29,10 +29,10 @@
<properties> <properties>
<main.dir>${basedir}</main.dir> <main.dir>${basedir}</main.dir>
<spring-boot.version>1.4.3.RELEASE</spring-boot.version> <spring-boot.version>2.1.3.RELEASE</spring-boot.version>
<spring.version>4.3.4.RELEASE</spring.version> <spring.version>5.1.5.RELEASE</spring.version>
<spring-security.version>4.2.0.RELEASE</spring-security.version> <spring-security.version>5.1.4.RELEASE</spring-security.version>
<spring-data-redis.version>1.8.10.RELEASE</spring-data-redis.version> <spring-data-redis.version>2.1.5.RELEASE</spring-data-redis.version>
<jedis.version>2.9.0</jedis.version> <jedis.version>2.9.0</jedis.version>
<jjwt.version>0.7.0</jjwt.version> <jjwt.version>0.7.0</jjwt.version>
<json-path.version>2.2.0</json-path.version> <json-path.version>2.2.0</json-path.version>
@ -41,8 +41,8 @@
<logback.version>1.2.3</logback.version> <logback.version>1.2.3</logback.version>
<mockito.version>1.9.5</mockito.version> <mockito.version>1.9.5</mockito.version>
<rat.version>0.10</rat.version> <rat.version>0.10</rat.version>
<cassandra.version>3.5.0</cassandra.version> <cassandra.version>3.6.0</cassandra.version>
<cassandra-unit.version>3.3.0.2</cassandra-unit.version> <cassandra-unit.version>3.5.0.1</cassandra-unit.version>
<takari-cpsuite.version>1.2.7</takari-cpsuite.version> <takari-cpsuite.version>1.2.7</takari-cpsuite.version>
<guava.version>21.0</guava.version> <guava.version>21.0</guava.version>
<caffeine.version>2.6.1</caffeine.version> <caffeine.version>2.6.1</caffeine.version>
@ -50,7 +50,7 @@
<commons-validator.version>1.5.0</commons-validator.version> <commons-validator.version>1.5.0</commons-validator.version>
<commons-io.version>2.5</commons-io.version> <commons-io.version>2.5</commons-io.version>
<commons-csv.version>1.4</commons-csv.version> <commons-csv.version>1.4</commons-csv.version>
<jackson.version>2.8.11.1</jackson.version> <jackson.version>2.9.7</jackson.version>
<json-schema-validator.version>2.2.6</json-schema-validator.version> <json-schema-validator.version>2.2.6</json-schema-validator.version>
<scala.version>2.11</scala.version> <scala.version>2.11</scala.version>
<akka.version>2.4.2</akka.version> <akka.version>2.4.2</akka.version>
@ -60,11 +60,11 @@
<velocity-tools.version>2.0</velocity-tools.version> <velocity-tools.version>2.0</velocity-tools.version>
<mail.version>1.4.3</mail.version> <mail.version>1.4.3</mail.version>
<curator.version>4.0.1</curator.version> <curator.version>4.0.1</curator.version>
<protobuf.version>3.0.2</protobuf.version> <protobuf.version>3.6.1</protobuf.version>
<grpc.version>1.12.0</grpc.version> <grpc.version>1.16.1</grpc.version>
<lombok.version>1.16.18</lombok.version> <lombok.version>1.16.18</lombok.version>
<paho.client.version>1.1.0</paho.client.version> <paho.client.version>1.1.0</paho.client.version>
<netty.version>4.1.22.Final</netty.version> <netty.version>4.1.30.Final</netty.version>
<os-maven-plugin.version>1.5.0</os-maven-plugin.version> <os-maven-plugin.version>1.5.0</os-maven-plugin.version>
<rabbitmq.version>4.8.0</rabbitmq.version> <rabbitmq.version>4.8.0</rabbitmq.version>
<surfire.version>2.19.1</surfire.version> <surfire.version>2.19.1</surfire.version>
@ -85,6 +85,8 @@
<kafka.version>2.0.0</kafka.version> <kafka.version>2.0.0</kafka.version>
<bucket4j.version>4.1.1</bucket4j.version> <bucket4j.version>4.1.1</bucket4j.version>
<fst.version>2.57</fst.version> <fst.version>2.57</fst.version>
<antlr.version>2.7.7</antlr.version>
<snakeyaml.version>1.23</snakeyaml.version>
</properties> </properties>
<modules> <modules>
@ -512,6 +514,16 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<dependency>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
<version>${antlr.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.rabbitmq</groupId> <groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId> <artifactId>amqp-client</artifactId>
@ -600,6 +612,16 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version> <version>${jackson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.github.fge</groupId> <groupId>com.github.fge</groupId>
<artifactId>json-schema-validator</artifactId> <artifactId>json-schema-validator</artifactId>
@ -794,6 +816,12 @@
<groupId>de.ruedigermoeller</groupId> <groupId>de.ruedigermoeller</groupId>
<artifactId>fst</artifactId> <artifactId>fst</artifactId>
<version>${fst.version}</version> <version>${fst.version}</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.springfox.ui</groupId> <groupId>io.springfox.ui</groupId>

3
ui/src/app/api/widget.service.js

@ -32,6 +32,7 @@ import TbAnalogueCompass from '../widget/lib/analogue-compass';
import TbCanvasDigitalGauge from '../widget/lib/canvas-digital-gauge'; import TbCanvasDigitalGauge from '../widget/lib/canvas-digital-gauge';
import TbMapWidget from '../widget/lib/map-widget'; import TbMapWidget from '../widget/lib/map-widget';
import TbMapWidgetV2 from '../widget/lib/map-widget2'; import TbMapWidgetV2 from '../widget/lib/map-widget2';
import TripAnimationWidget from '../widget/lib/tripAnimation/trip-animation-widget';
import 'jquery.terminal/js/jquery.terminal.min.js'; import 'jquery.terminal/js/jquery.terminal.min.js';
import 'jquery.terminal/css/jquery.terminal.min.css'; import 'jquery.terminal/css/jquery.terminal.min.css';
@ -43,7 +44,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service'; import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils]) thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
.factory('widgetService', WidgetService) .factory('widgetService', WidgetService)
.name; .name;

8
ui/src/app/widget/lib/map-widget2.js

@ -546,7 +546,13 @@ export default class TbMapWidgetV2 {
function mapPolygonArray (rawArray) { function mapPolygonArray (rawArray) {
let latLngArray = rawArray.map(function (el) { let latLngArray = rawArray.map(function (el) {
if (el.length === 2) { if (el.length === 2) {
return tbMap.map.createLatLng(el[0], el[1]); if (!angular.isNumber(el[0]) && !angular.isNumber(el[1])) {
return el.map(function (subEl) {
return mapPolygonArray(subEl);
})
} else {
return tbMap.map.createLatLng(el[0], el[1]);
}
} else if (el.length > 2) { } else if (el.length > 2) {
return mapPolygonArray(el); return mapPolygonArray(el);
} else { } else {

702
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js

File diff suppressed because one or more lines are too long

106
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss

@ -0,0 +1,106 @@
/**
* Copyright © 2016-2019 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.
*/
.heat-map-widget {
position: relative;
width: 100%;
height: 100%;
font-size: 16px;
line-height: 24px;
}
.heat-map-info-panel {
position: absolute;
top: 0;
right: 0;
z-index: 2;
background-color: rgba(0, 0, 0, .3);
border-bottom-left-radius: 5px;
.md-button {
min-width: auto;
}
}
.heat-map-tooltip {
position: absolute;
top: 47px;
right: 0;
z-index: 2;
padding: 10px;
background-color: #fff;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
transition: .3s ease-in-out;
&-hidden {
transform: translateX(100%);
}
}
.heat-map-title {
padding: 10px;
}
.heat-map-control-panel {
position: absolute;
bottom: 0;
z-index: 2;
box-sizing: border-box;
width: 100%;
padding-right: 70px;
padding-left: 20px;
background: rgba(0, 0, 0, .3);
md-slider-container {
button {
max-width: none;
ng-md-icon {
width: 36px;
height: 36px;
svg {
width: inherit;
height: inherit;
}
}
}
.panel-timer {
max-width: none;
font-size: 20px;
font-weight: 600;
}
}
}
.heat-map-container {
position: relative;
z-index: 1;
flex: 1;
width: 100%;
}
#heat-map {
z-index: 1;
width: 100%;
height: 100%;
.pointsLayerMarkerIcon {
border-radius: 50%;
}
}

54
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2019 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.
-->
<div class="heat-map-widget tracking-widget" layout="column" layout-align="center start"
ng-style="{'padding-top' : (!vm.widgetConfig.showTitle && vm.ctx.settings.showLabel) ? '5px' : '0px'}">
<div ng-show="vm.ctx.settings.showLabel">
{{vm.trips[vm.activeTripIndex].settings.labelText}}
</div>
<div class="heat-map-control-panel" lang="row" layout-align="center start">
<md-slider-container>
<md-slider ng-model="vm.index" min="{{vm.minTime}}" max="{{vm.maxTime}}" ng-change="vm.recalculateTrips()"></md-slider>
<md-button aria-label="Play" ng-click="vm.playMove(true)" ng-disabled="vm.isPlaying">
<ng-md-icon icon="play_circle_outline"></ng-md-icon>
</md-button>
<md-select ng-model="vm.speed" aria-label="Speed selector">
<md-option ng-value="speed" flex="1" ng-repeat="speed in vm.speeds track by $index">{{ speed}}
</md-option>
</md-select>
<md-button aria-label="Stop playing" ng-click="vm.stopPlay()">
<ng-md-icon icon="pause_circle_outline"></ng-md-icon>
</md-button>
<div class="panel-timer">{{vm.trips[vm.activeTripIndex].timeRange[vm.index].ts | date:'medium'}}
</div>
</md-slider-container>
</div>
<div class="heat-map-container" layout="column">
<div flex id='heat-map'></div>
<div class="heat-map-info-panel" layout="row">
<md-button aria-label="tooltip" ng-show="vm.staticSettings.displayTooltip" ng-click="vm.showHideTooltip()">
<ng-md-icon icon="info"></ng-md-icon>
</md-button>
<!--<md-button aria-label="settings" ng-click="vm.openHideSettings()">-->
<!--<ng-md-icon icon="settings_applications"></ng-md-icon>-->
<!--</md-button>-->
</div>
<div class="heat-map-tooltip" class="heat-map-title" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'heat-map-tooltip-hidden':''" ng-bind-html="vm.trips[vm.activeTripIndex].settings.tooltipText"
ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}">
</div>
</div>
</div>
Loading…
Cancel
Save