Browse Source

Merge remote-tracking branch 'upstream/master' into feacher/add_map_here

pull/1610/head
Vladyslav_Prykhodko 7 years ago
parent
commit
865e3f31dd
  1. 139
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 29
      application/src/main/java/org/thingsboard/server/actors/tenant/DebugTbRateLimits.java
  3. 27
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  4. 5
      application/src/main/resources/thingsboard.yml
  5. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  6. 47
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  7. 1
      dao/src/test/resources/cassandra-test.properties
  8. 84
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java
  9. 83
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java
  10. 28
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java
  11. 28
      ui/package-lock.json
  12. 10
      ui/src/app/common/types.constant.js
  13. 18
      ui/src/app/components/legend-config-panel.controller.js
  14. 20
      ui/src/app/components/legend-config-panel.tpl.html
  15. 3
      ui/src/app/components/legend-config.directive.js
  16. 4
      ui/src/app/components/legend-config.scss
  17. 2
      ui/src/app/components/legend.directive.js
  18. 4
      ui/src/app/components/legend.scss
  19. 4
      ui/src/app/components/legend.tpl.html
  20. 5
      ui/src/app/locale/locale.constant-en_US.json
  21. 5
      ui/src/app/locale/locale.constant-ru_RU.json
  22. 5
      ui/src/app/locale/locale.constant-uk_UA.json
  23. 6
      ui/src/app/locale/locale.constant-zh_CN.json
  24. 3
      ui/src/app/widget/lib/openstreet-map.js
  25. 1403
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js
  26. 1
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss
  27. 2
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html
  28. 2
      ui/src/app/widget/lib/widget-utils.js

139
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleChainTransactionService;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
@ -84,6 +86,8 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Slf4j
@Component
@ -92,6 +96,12 @@ public class ActorSystemContext {
protected final ObjectMapper mapper = new ObjectMapper();
private final ConcurrentMap<TenantId, DebugTbRateLimits> debugPerTenantLimits = new ConcurrentHashMap<>();
public ConcurrentMap<TenantId, DebugTbRateLimits> getDebugPerTenantLimits() {
return debugPerTenantLimits;
}
@Getter
@Setter
private ActorService actorService;
@ -291,6 +301,14 @@ public class ActorSystemContext {
@Getter
private long sessionReportTimeout;
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
@Getter
private boolean debugPerTenantEnabled;
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.configuration}")
@Getter
private String debugPerTenantLimitsConfiguration;
@Getter
@Setter
private ActorSystem actorSystem;
@ -318,8 +336,6 @@ public class ActorSystemContext {
@Getter
private CassandraBufferedRateExecutor cassandraBufferedRateExecutor;
public ActorSystemContext() {
config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
}
@ -392,46 +408,97 @@ public class ActorSystemContext {
}
private void persistDebugAsync(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, String relationType, Throwable error) {
try {
Event event = new Event();
event.setTenantId(tenantId);
event.setEntityId(entityId);
event.setType(DataConstants.DEBUG_RULE_NODE);
String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
ObjectNode node = mapper.createObjectNode()
.put("type", type)
.put("server", getServerAddress())
.put("entityId", tbMsg.getOriginator().getId().toString())
.put("entityName", tbMsg.getOriginator().getEntityType().name())
.put("msgId", tbMsg.getId().toString())
.put("msgType", tbMsg.getType())
.put("dataType", tbMsg.getDataType().name())
.put("relationType", relationType)
.put("data", tbMsg.getData())
.put("metadata", metadata);
if (error != null) {
node = node.put("error", toString(error));
if (checkLimits(tenantId, tbMsg, error)) {
try {
Event event = new Event();
event.setTenantId(tenantId);
event.setEntityId(entityId);
event.setType(DataConstants.DEBUG_RULE_NODE);
String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
ObjectNode node = mapper.createObjectNode()
.put("type", type)
.put("server", getServerAddress())
.put("entityId", tbMsg.getOriginator().getId().toString())
.put("entityName", tbMsg.getOriginator().getEntityType().name())
.put("msgId", tbMsg.getId().toString())
.put("msgType", tbMsg.getType())
.put("dataType", tbMsg.getDataType().name())
.put("relationType", relationType)
.put("data", tbMsg.getData())
.put("metadata", metadata);
if (error != null) {
node = node.put("error", toString(error));
}
event.setBody(node);
ListenableFuture<Event> future = eventService.saveAsync(event);
Futures.addCallback(future, new FutureCallback<Event>() {
@Override
public void onSuccess(@Nullable Event event) {
}
@Override
public void onFailure(Throwable th) {
log.error("Could not save debug Event for Node", th);
}
});
} catch (IOException ex) {
log.warn("Failed to persist rule node debug message", ex);
}
}
}
event.setBody(node);
ListenableFuture<Event> future = eventService.saveAsync(event);
Futures.addCallback(future, new FutureCallback<Event>() {
@Override
public void onSuccess(@Nullable Event event) {
private boolean checkLimits(TenantId tenantId, TbMsg tbMsg, Throwable error) {
if (debugPerTenantEnabled) {
DebugTbRateLimits debugTbRateLimits = debugPerTenantLimits.computeIfAbsent(tenantId, id ->
new DebugTbRateLimits(new TbRateLimits(debugPerTenantLimitsConfiguration), false));
if (!debugTbRateLimits.getTbRateLimits().tryConsume()) {
if (!debugTbRateLimits.isRuleChainEventSaved()) {
persistRuleChainDebugModeEvent(tenantId, tbMsg.getRuleChainId(), error);
debugTbRateLimits.setRuleChainEventSaved(true);
}
@Override
public void onFailure(Throwable th) {
log.error("Could not save debug Event for Node", th);
if (log.isTraceEnabled()) {
log.trace("[{}] Tenant level debug mode rate limit detected: {}", tenantId, tbMsg);
}
});
} catch (IOException ex) {
log.warn("Failed to persist rule node debug message", ex);
return false;
}
}
return true;
}
private void persistRuleChainDebugModeEvent(TenantId tenantId, EntityId entityId, Throwable error) {
Event event = new Event();
event.setTenantId(tenantId);
event.setEntityId(entityId);
event.setType(DataConstants.DEBUG_RULE_CHAIN);
ObjectNode node = mapper.createObjectNode()
//todo: what fields are needed here?
.put("server", getServerAddress())
.put("message", "Reached debug mode rate limit!");
if (error != null) {
node = node.put("error", toString(error));
}
event.setBody(node);
ListenableFuture<Event> future = eventService.saveAsync(event);
Futures.addCallback(future, new FutureCallback<Event>() {
@Override
public void onSuccess(@Nullable Event event) {
}
@Override
public void onFailure(Throwable th) {
log.error("Could not save debug Event for Rule Chain", th);
}
});
}
public static Exception toException(Throwable error) {

29
application/src/main/java/org/thingsboard/server/actors/tenant/DebugTbRateLimits.java

@ -0,0 +1,29 @@
/**
* 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.actors.tenant;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
@Data
@AllArgsConstructor
public class DebugTbRateLimits {
private TbRateLimits tbRateLimits;
private boolean ruleChainEventSaved;
}

27
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
@ -34,6 +35,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Event;
@ -56,10 +59,10 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
@Slf4j
@ -78,6 +81,12 @@ public class RuleChainController extends BaseController {
@Autowired
private JsInvokeService jsInvokeService;
@Autowired(required = false)
private ActorSystemContext actorContext;
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
private boolean debugPerTenantEnabled;
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
@ResponseBody
@ -182,8 +191,17 @@ public class RuleChainController extends BaseController {
@ResponseBody
public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
try {
TenantId tenantId = getTenantId();
if (debugPerTenantEnabled) {
ConcurrentMap<TenantId, DebugTbRateLimits> debugPerTenantLimits = actorContext.getDebugPerTenantLimits();
DebugTbRateLimits debugTbRateLimits = debugPerTenantLimits.getOrDefault(tenantId, null);
if (debugTbRateLimits != null) {
debugPerTenantLimits.remove(tenantId, debugTbRateLimits);
}
}
RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId(), Operation.WRITE);
RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(getTenantId(), ruleChainMetaData));
RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData));
actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED);
@ -236,7 +254,7 @@ public class RuleChainController extends BaseController {
referencingRuleChainIds.remove(ruleChain.getId());
referencingRuleChainIds.forEach(referencingRuleChainId ->
actorService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED));
actorService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED));
actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED);
@ -291,7 +309,8 @@ public class RuleChainController extends BaseController {
String data = inputParams.get("msg").asText();
JsonNode metadataJson = inputParams.get("metadata");
Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {});
Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {
});
String msgType = inputParams.get("msgType").asText();
String output = "";
String errorText = "";

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

@ -173,6 +173,8 @@ cassandra:
callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}"
poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}"
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data types values except target to null for the same ts on save
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}"
tenant_rate_limits:
enabled: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_ENABLED:false}"
configuration: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION:1000:1,30000:60}"
@ -210,6 +212,9 @@ actors:
chain:
# Errors for particular actor are persisted once per specified amount of milliseconds
error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"
debug_mode_rate_limits_per_tenant:
enabled: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}"
configuration: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:500:3600}"
node:
# Errors for particular actor are persisted once per specified amount of milliseconds
error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}"

1
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -38,6 +38,7 @@ public class DataConstants {
public static final String LC_EVENT = "LC_EVENT";
public static final String STATS = "STATS";
public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
public static final String DEBUG_RULE_CHAIN = "DEBUG_RULE_CHAIN";
public static final String ONEWAY = "ONEWAY";
public static final String TWOWAY = "TWOWAY";

47
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -94,6 +94,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
@Value("${cassandra.query.ts_key_value_ttl}")
private long systemTtl;
@Value("${cassandra.query.set_null_values_enabled}")
private boolean setNullValuesEnabled;
private TsPartitionDate tsFormat;
private PreparedStatement partitionInsertStmt;
@ -307,9 +310,13 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
@Override
public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) {
List<ListenableFuture<Void>> futures = new ArrayList<>();
ttl = computeTtl(ttl);
long partition = toPartitionTs(tsKvEntry.getTs());
DataType type = tsKvEntry.getDataType();
if (setNullValuesEnabled) {
processSetNullValues(tenantId, entityId, tsKvEntry, ttl, futures, partition, type);
}
BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind();
stmt.setString(0, entityId.getEntityType().name())
.setUUID(1, entityId.getId())
@ -320,6 +327,46 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
if (ttl > 0) {
stmt.setInt(6, (int) ttl);
}
futures.add(getFuture(executeAsyncWrite(tenantId, stmt), rs -> null));
return Futures.transform(Futures.allAsList(futures), result -> null);
}
private void processSetNullValues(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, List<ListenableFuture<Void>> futures, long partition, DataType type) {
switch (type) {
case LONG:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
break;
case BOOLEAN:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
break;
case DOUBLE:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
break;
case STRING:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
break;
}
}
private ListenableFuture<Void> saveNull(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, long partition, DataType type) {
BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind();
stmt.setString(0, entityId.getEntityType().name())
.setUUID(1, entityId.getId())
.setString(2, tsKvEntry.getKey())
.setLong(3, partition)
.setLong(4, tsKvEntry.getTs());
stmt.setToNull(getColumnName(type));
if (ttl > 0) {
stmt.setInt(6, (int) ttl);
}
return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null);
}

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

@ -53,6 +53,7 @@ cassandra.query.buffer_size=100000
cassandra.query.concurrent_limit=1000
cassandra.query.permit_max_wait_time=20000
cassandra.query.rate_limit_print_interval_ms=30000
cassandra.query.set_null_values_enabled=false
cassandra.query.tenant_rate_limits.enabled=false
cassandra.query.tenant_rate_limits.configuration=5000:1,100000:60
cassandra.query.tenant_rate_limits.print_tenant_names=false

84
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@ -34,9 +36,9 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
@Slf4j
public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration> implements TbNode {
@ -54,19 +56,20 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
try {
ctx.tellNext(getDetails(ctx, msg), SUCCESS);
} catch (Exception e) {
ctx.tellFailure(msg, e);
}
withCallback(getDetails(ctx, msg),
m -> ctx.tellNext(m, SUCCESS),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
@Override
public void destroy() {}
public void destroy() {
}
protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;
protected abstract TbMsg getDetails(TbContext ctx, TbMsg msg);
protected abstract ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg);
protected abstract ListenableFuture<ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg);
protected MessageData getDataAsJson(TbMsg msg) {
if (this.config.isAddToMetadata()) {
@ -76,25 +79,56 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
}
}
protected TbMsg transformMsg(TbContext ctx, TbMsg msg, JsonElement resultObject, MessageData messageData) {
if (messageData.getDataType().equals("metadata")) {
Map<String, String> metadataMap = gson.fromJson(resultObject.toString(), TYPE);
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData());
protected ListenableFuture<TbMsg> getTbMsgListenableFuture(TbContext ctx, TbMsg msg, MessageData messageData, String prefix) {
if (!this.config.getDetailsList().isEmpty()) {
ListenableFuture<JsonElement> resultObject = null;
ListenableFuture<ContactBased> contactBasedListenableFuture = getContactBasedListenableFuture(ctx, msg);
for (EntityDetails entityDetails : this.config.getDetailsList()) {
resultObject = addContactProperties(messageData.getData(), contactBasedListenableFuture, entityDetails, prefix);
}
return transformMsg(ctx, msg, resultObject, messageData);
} else {
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(resultObject));
return Futures.immediateFuture(msg);
}
}
protected JsonElement addContactProperties(JsonElement data, ContactBased entity, EntityDetails entityDetails, String prefix) {
private ListenableFuture<TbMsg> transformMsg(TbContext ctx, TbMsg msg, ListenableFuture<JsonElement> propertiesFuture, MessageData messageData) {
return Futures.transformAsync(propertiesFuture, jsonElement -> {
if (jsonElement != null) {
if (messageData.getDataType().equals("metadata")) {
Map<String, String> metadataMap = gson.fromJson(jsonElement.toString(), TYPE);
return Futures.immediateFuture(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData()));
} else {
return Futures.immediateFuture(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(jsonElement)));
}
} else {
return Futures.immediateFuture(null);
}
});
}
private ListenableFuture<JsonElement> addContactProperties(JsonElement data, ListenableFuture<ContactBased> entityFuture, EntityDetails entityDetails, String prefix) {
return Futures.transformAsync(entityFuture, contactBased -> {
if (contactBased != null) {
return Futures.immediateFuture(setProperties(contactBased, data, entityDetails, prefix));
} else {
return Futures.immediateFuture(null);
}
});
}
private JsonElement setProperties(ContactBased entity, JsonElement data, EntityDetails entityDetails, String prefix) {
JsonObject dataAsObject = data.getAsJsonObject();
switch (entityDetails) {
case ADDRESS:
if (entity.getAddress() != null)
if (entity.getAddress() != null) {
dataAsObject.addProperty(prefix + "address", entity.getAddress());
}
break;
case ADDRESS2:
if (entity.getAddress2() != null)
if (entity.getAddress2() != null) {
dataAsObject.addProperty(prefix + "address2", entity.getAddress2());
}
break;
case CITY:
if (entity.getCity() != null) dataAsObject.addProperty(prefix + "city", entity.getCity());
@ -104,16 +138,24 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
dataAsObject.addProperty(prefix + "country", entity.getCountry());
break;
case STATE:
if (entity.getState() != null) dataAsObject.addProperty(prefix + "state", entity.getState());
if (entity.getState() != null) {
dataAsObject.addProperty(prefix + "state", entity.getState());
}
break;
case EMAIL:
if (entity.getEmail() != null) dataAsObject.addProperty(prefix + "email", entity.getEmail());
if (entity.getEmail() != null) {
dataAsObject.addProperty(prefix + "email", entity.getEmail());
}
break;
case PHONE:
if (entity.getPhone() != null) dataAsObject.addProperty(prefix + "phone", entity.getPhone());
if (entity.getPhone() != null) {
dataAsObject.addProperty(prefix + "phone", entity.getPhone());
}
break;
case ZIP:
if (entity.getZip() != null) dataAsObject.addProperty(prefix + "zip", entity.getZip());
if (entity.getZip() != null) {
dataAsObject.addProperty(prefix + "zip", entity.getZip());
}
break;
case ADDITIONAL_INFO:
if (entity.getAdditionalInfo().hasNonNull("description")) {
@ -126,7 +168,7 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
@Data
@AllArgsConstructor
protected static class MessageData {
private static class MessageData {
private JsonElement data;
private String dataType;
}

83
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java

@ -15,19 +15,16 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityDetails;
import org.thingsboard.server.common.data.ContactBased;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
@ -54,45 +51,59 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbG
}
@Override
protected TbMsg getDetails(TbContext ctx, TbMsg msg) {
return getCustomerTbMsg(ctx, msg, getDataAsJson(msg));
protected ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg) {
return getTbMsgListenableFuture(ctx, msg, getDataAsJson(msg), CUSTOMER_PREFIX);
}
private TbMsg getCustomerTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) {
JsonElement resultObject = null;
if (!config.getDetailsList().isEmpty()) {
for (EntityDetails entityDetails : config.getDetailsList()) {
resultObject = addContactProperties(messageData.getData(), getCustomer(ctx, msg), entityDetails, CUSTOMER_PREFIX);
@Override
protected ListenableFuture<ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg) {
return Futures.transformAsync(getCustomer(ctx, msg), customer -> {
if (customer != null) {
return Futures.immediateFuture(customer);
} else {
return Futures.immediateFuture(null);
}
return transformMsg(ctx, msg, resultObject, messageData);
} else {
return msg;
}
});
}
private Customer getCustomer(TbContext ctx, TbMsg msg) {
private ListenableFuture<Customer> getCustomer(TbContext ctx, TbMsg msg) {
switch (msg.getOriginator().getEntityType()) {
case DEVICE:
Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()));
if (!device.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), device.getCustomerId());
} else {
throw new RuntimeException("Device with name '" + device.getName() + "' is not assigned to Customer.");
}
return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId())), device -> {
if (device != null) {
if (!device.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), device.getCustomerId());
} else {
throw new RuntimeException("Device with name '" + device.getName() + "' is not assigned to Customer.");
}
} else {
return Futures.immediateFuture(null);
}
});
case ASSET:
Asset asset = ctx.getAssetService().findAssetById(ctx.getTenantId(), new AssetId(msg.getOriginator().getId()));
if (!asset.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), asset.getCustomerId());
} else {
throw new RuntimeException("Asset with name '" + asset.getName() + "' is not assigned to Customer.");
}
return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(msg.getOriginator().getId())), asset -> {
if (asset != null) {
if (!asset.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), asset.getCustomerId());
} else {
throw new RuntimeException("Asset with name '" + asset.getName() + "' is not assigned to Customer.");
}
} else {
return Futures.immediateFuture(null);
}
});
case ENTITY_VIEW:
EntityView entityView = ctx.getEntityViewService().findEntityViewById(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId()));
if (!entityView.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), entityView.getCustomerId());
} else {
throw new RuntimeException("EntityView with name '" + entityView.getName() + "' is not assigned to Customer.");
}
return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId())), entityView -> {
if (entityView != null) {
if (!entityView.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), entityView.getCustomerId());
} else {
throw new RuntimeException("EntityView with name '" + entityView.getName() + "' is not assigned to Customer.");
}
} else {
return Futures.immediateFuture(null);
}
});
default:
throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported.");
}

28
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java

@ -15,17 +15,15 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityDetails;
import org.thingsboard.server.common.data.ContactBased;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@ -49,20 +47,18 @@ public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGet
}
@Override
protected TbMsg getDetails(TbContext ctx, TbMsg msg) {
return getTenantTbMsg(ctx, msg, getDataAsJson(msg));
protected ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg) {
return getTbMsgListenableFuture(ctx, msg, getDataAsJson(msg), TENANT_PREFIX);
}
private TbMsg getTenantTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) {
JsonElement resultObject = null;
Tenant tenant = ctx.getTenantService().findTenantById(ctx.getTenantId());
if (!config.getDetailsList().isEmpty()) {
for (EntityDetails entityDetails : config.getDetailsList()) {
resultObject = addContactProperties(messageData.getData(), tenant, entityDetails, TENANT_PREFIX);
@Override
protected ListenableFuture<ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg) {
return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), ctx.getTenantId()), tenant -> {
if (tenant != null) {
return Futures.immediateFuture(tenant);
} else {
return Futures.immediateFuture(null);
}
return transformMsg(ctx, msg, resultObject, messageData);
} else {
return msg;
}
});
}
}

28
ui/package-lock.json

@ -5239,14 +5239,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5261,20 +5259,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -5391,8 +5386,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -5404,7 +5398,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5419,7 +5412,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5427,14 +5419,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -5453,7 +5443,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5534,8 +5523,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -5547,7 +5535,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5669,7 +5656,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",

10
ui/src/app/common/types.constant.js

@ -276,6 +276,16 @@ export default angular.module('thingsboard.types', [])
name: 'alias.filter-type-entity-view-search-query'
}
},
direction: {
column: {
value: "column",
name: "direction.column"
},
row: {
value: "row",
name: "direction.row"
}
},
position: {
top: {
value: "top",

18
ui/src/app/components/legend-config-panel.controller.js

@ -21,10 +21,26 @@ export default function LegendConfigPanelController(mdPanelRef, $scope, types, l
vm.legendConfig = legendConfig;
vm.onLegendConfigUpdate = onLegendConfigUpdate;
vm.positions = types.position;
vm.directions = types.direction;
vm.isRowDirection = vm.legendConfig.direction === types.direction.row.value;
vm._mdPanelRef.config.onOpenComplete = function () {
$scope.theForm.$setPristine();
}
};
vm.onChangeDirection = function() {
if (vm.legendConfig.direction === types.direction.row.value) {
vm.isRowDirection = true;
vm.legendConfig.position = types.position.bottom.value;
vm.legendConfig.showMin = false;
vm.legendConfig.showMax = false;
vm.legendConfig.showAvg = false;
vm.legendConfig.showTotal = false;
}
else {
vm.isRowDirection = false;
}
};
$scope.$watch('vm.legendConfig', function () {
if (onLegendConfigUpdate) {

20
ui/src/app/components/legend-config-panel.tpl.html

@ -20,24 +20,34 @@
<md-content style="height: 100%" flex layout="column">
<section layout="column">
<md-content class="md-padding" layout="column">
<md-input-container>
<label translate>legend.direction</label>
<md-select ng-model="vm.legendConfig.direction" style="min-width: 150px;"
ng-change="vm.onChangeDirection()">
<md-option ng-repeat="direction in vm.directions" ng-value="direction.value">
{{direction.name | translate}}
</md-option>
</md-select>
</md-input-container>
<md-input-container>
<label translate>legend.position</label>
<md-select ng-model="vm.legendConfig.position" style="min-width: 150px;">
<md-option ng-repeat="pos in vm.positions" ng-value="pos.value">
<md-option ng-repeat="pos in vm.positions" ng-value="pos.value"
ng-disabled="(vm.isRowDirection && (pos.value === vm.positions.left.value || pos.value === vm.positions.right.value))">
{{pos.name | translate}}
</md-option>
</md-select>
</md-input-container>
<md-checkbox flex aria-label="{{ 'legend.show-min' | translate }}"
<md-checkbox flex aria-label="{{ 'legend.show-min' | translate }}" ng-disabled="vm.isRowDirection"
ng-model="vm.legendConfig.showMin">{{ 'legend.show-min' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'legend.show-max' | translate }}"
<md-checkbox flex aria-label="{{ 'legend.show-max' | translate }}" ng-disabled="vm.isRowDirection"
ng-model="vm.legendConfig.showMax">{{ 'legend.show-max' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'legend.show-avg' | translate }}"
<md-checkbox flex aria-label="{{ 'legend.show-avg' | translate }}" ng-disabled="vm.isRowDirection"
ng-model="vm.legendConfig.showAvg">{{ 'legend.show-avg' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'legend.show-total' | translate }}"
<md-checkbox flex aria-label="{{ 'legend.show-total' | translate }}" ng-disabled="vm.isRowDirection"
ng-model="vm.legendConfig.showTotal">{{ 'legend.show-total' | translate }}
</md-checkbox>
</md-content>

3
ui/src/app/components/legend-config.directive.js

@ -102,6 +102,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) {
scope.updateView = function () {
var value = {};
var model = scope.model;
value.direction = model.direction;
value.position = model.position;
value.showMin = model.showMin;
value.showMax = model.showMax;
@ -117,6 +118,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) {
scope.model = {};
}
var model = scope.model;
model.direction = value.direction || types.direction.column.value;
model.position = value.position || types.position.bottom.value;
model.showMin = angular.isDefined(value.showMin) ? value.showMin : false;
model.showMax = angular.isDefined(value.showMax) ? value.showMax : false;
@ -124,6 +126,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) {
model.showTotal = angular.isDefined(value.showTotal) ? value.showTotal : false;
} else {
scope.model = {
direction: types.direction.column.value,
position: types.position.bottom.value,
showMin: false,
showMax: false,

4
ui/src/app/components/legend-config.scss

@ -21,7 +21,7 @@
.tb-legend-config-panel {
min-width: 220px;
max-height: 220px;
max-height: 300px;
overflow: hidden;
background: #fff;
border-radius: 4px;
@ -41,7 +41,7 @@
}
.md-padding {
padding: 0 16px;
padding: 12px 16px 0;
}
}

2
ui/src/app/components/legend.directive.js

@ -43,6 +43,8 @@ function Legend($compile, $templateCache, types) {
scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
scope.legendConfig.position === types.position.top.value;
scope.isRowDirection = scope.legendConfig.direction === types.direction.row.value;
scope.toggleHideData = function(index) {
scope.legendData.keys[index].dataKey.hidden = !scope.legendData.keys[index].dataKey.hidden;
}

4
ui/src/app/components/legend.scss

@ -57,5 +57,9 @@ table.tb-legend {
opacity: .6;
}
}
&.tb-row-direction {
display: inline-block;
}
}
}

4
ui/src/app/components/legend.tpl.html

@ -17,7 +17,7 @@
-->
<table class="tb-legend">
<thead>
<tr class="tb-legend-header">
<tr class="tb-legend-header" ng-if="!isRowDirection">
<th colspan="2"></th>
<th ng-if="legendConfig.showMin === true">{{ 'legend.min' | translate }}</th>
<th ng-if="legendConfig.showMax === true">{{ 'legend.max' | translate }}</th>
@ -26,7 +26,7 @@
</tr>
</thead>
<tbody>
<tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys">
<tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys" ng-class="{ 'tb-row-direction': isRowDirection }">
<td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.dataKey.color}"></span></td>
<td class="tb-legend-label"
ng-click="toggleHideData(legendKey.dataIndex)"

5
ui/src/app/locale/locale.constant-en_US.json

@ -670,6 +670,10 @@
"dialog": {
"close": "Close dialog"
},
"direction": {
"column": "Column",
"row": "Row"
},
"error": {
"unable-to-connect": "Unable to connect to the server! Please check your internet connection.",
"unhandled-error-code": "Unhandled error code: {{errorCode}}",
@ -1116,6 +1120,7 @@
"select": "Select target layout"
},
"legend": {
"direction": "Legend direction",
"position": "Legend position",
"show-max": "Show max value",
"show-min": "Show min value",

5
ui/src/app/locale/locale.constant-ru_RU.json

@ -670,6 +670,10 @@
"dialog": {
"close": "Закрыть диалог"
},
"direction": {
"column": "Колонка",
"row": "Строка"
},
"error": {
"unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.",
"unhandled-error-code": "Код необработанной ошибки: {{errorCode}}",
@ -1109,6 +1113,7 @@
"select": "Выбрать макет"
},
"legend": {
"direction": "Расположение элементов легенды",
"position": "Расположение легенды",
"show-max": "Показать максимальное значение",
"show-min": "Показать минимальное значение",

5
ui/src/app/locale/locale.constant-uk_UA.json

@ -795,6 +795,10 @@
"dialog": {
"close": "Закрити діалогове вікно"
},
"direction": {
"column": "Колонка",
"row": "Рядок"
},
"error": {
"unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.",
"unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}",
@ -1526,6 +1530,7 @@
"select": "Вибрати макет"
},
"legend": {
"direction": "Розташування елементів легенди",
"position": "Розташування легенди",
"show-max": "Показати максимальне значення",
"show-min": "Показати мінімальне значення ",

6
ui/src/app/locale/locale.constant-zh_CN.json

@ -1442,7 +1442,7 @@
"Dec": "12月",
"January": "一月",
"February": "二月",
"March": "游行",
"March": "三月",
"April": "四月",
"June": "六月",
"July": "七月",
@ -1472,7 +1472,7 @@
"6 months": "6个月",
"Custom interval": "自定义间隔",
"Interval": "间隔",
"Step size": "一步的大小",
"Step size": "步长",
"Ok": "Ok"
}
}
@ -1509,4 +1509,4 @@
"uk_UA": "乌克兰"
}
}
}
}

3
ui/src/app/widget/lib/openstreet-map.js

@ -29,7 +29,7 @@ export default class TbOpenStreetMap {
if (!mapProvider) {
mapProvider = {
name: "OpenStreetMap.Mapnik"
name: "OpenStreetMap.Mapnik"
};
}
@ -41,7 +41,6 @@ export default class TbOpenStreetMap {
this.map = L.map($containerElement[0]).setView([0, 0], this.defaultZoomLevel || 8);
var tileLayer = mapProvider.isCustom ? L.tileLayer(mapProvider.name) : L.tileLayer.provider(mapProvider.name, credentials);
tileLayer.addTo(this.map);
if (initCallback) {

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

File diff suppressed because one or more lines are too long

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

@ -91,6 +91,7 @@
position: relative;
box-sizing: border-box;
width: 100%;
padding-bottom: 16px;
padding-left: 10px;
md-slider-container {

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

@ -27,7 +27,7 @@
<ng-md-icon icon="info_outline"></ng-md-icon>
</md-button>
</div>
<div class="trip-animation-tooltip md-whiteframe-z4" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'trip-animation-tooltip-hidden':''" ng-bind-html="vm.trips[vm.activeTripIndex].settings.tooltipText"
<div class="trip-animation-tooltip md-whiteframe-z4" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'trip-animation-tooltip-hidden':''" ng-bind-html="vm.staticSettings.tooltipMarker === 'polygon' ? vm.trips[vm.activeTripIndex].settings.polygonTooltipText : vm.trips[vm.activeTripIndex].settings.tooltipText"
ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}">
</div>
</div>

2
ui/src/app/widget/lib/widget-utils.js

@ -210,7 +210,7 @@ export function arraysEqual(a, b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (!a[i].equals(b[i])) return false;
if (!arraysEqual(a[i],b[i])) return false;
}
return true;
}

Loading…
Cancel
Save