Browse Source

maps widget bundle changes (trip_animation configs)

pull/1669/head
Maksym Dudnik 7 years ago
parent
commit
388b7be423
  1. 2
      application/src/main/data/json/system/widget_bundles/maps.json
  2. 139
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 29
      application/src/main/java/org/thingsboard/server/actors/tenant/DebugTbRateLimits.java
  4. 27
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  5. 6
      application/src/main/resources/thingsboard.yml
  6. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  7. 6
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  8. 10
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  9. 12
      dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
  10. 2
      dao/src/test/resources/cassandra-test.properties
  11. 2
      pom.xml
  12. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  13. 84
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java
  14. 83
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java
  15. 28
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java
  16. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  17. 1
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  18. 28
      ui/package-lock.json
  19. 10
      ui/src/app/common/types.constant.js
  20. 18
      ui/src/app/components/legend-config-panel.controller.js
  21. 20
      ui/src/app/components/legend-config-panel.tpl.html
  22. 3
      ui/src/app/components/legend-config.directive.js
  23. 4
      ui/src/app/components/legend-config.scss
  24. 2
      ui/src/app/components/legend.directive.js
  25. 4
      ui/src/app/components/legend.scss
  26. 4
      ui/src/app/components/legend.tpl.html
  27. 4
      ui/src/app/locale/locale.constant-de_DE.json
  28. 5
      ui/src/app/locale/locale.constant-en_US.json
  29. 5
      ui/src/app/locale/locale.constant-ru_RU.json
  30. 5
      ui/src/app/locale/locale.constant-uk_UA.json
  31. 102
      ui/src/app/locale/locale.constant-zh_CN.json
  32. 4
      ui/src/app/widget/lib/google-map.js
  33. 4
      ui/src/app/widget/lib/image-map.js
  34. 103
      ui/src/app/widget/lib/map-widget2.js
  35. 16
      ui/src/app/widget/lib/openstreet-map.js
  36. 4
      ui/src/app/widget/lib/tencent-map.js
  37. 11
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js

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

File diff suppressed because one or more lines are too long

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 = "";

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

@ -166,6 +166,8 @@ cassandra:
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
ts_key_value_ttl: "${TS_KV_TTL:0}"
events_ttl: "${TS_EVENTS_TTL:0}"
# Specify TTL of debug log in seconds. The current value corresponds to one week
debug_events_ttl: "${DEBUG_EVENTS_TTL:604800}"
buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
@ -212,6 +214,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}"
@ -466,6 +471,7 @@ transport:
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
so_keep_alive: "${NETTY_SO_KEEPALIVE:false}"
# MQTT SSL configuration
ssl:
# Enable/disable SSL support

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";

6
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.transport.mqtt;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@ -50,6 +51,8 @@ public class MqttTransportService {
private Integer bossGroupThreadCount;
@Value("${transport.mqtt.netty.worker_group_thread_count}")
private Integer workerGroupThreadCount;
@Value("${transport.mqtt.netty.so_keep_alive}")
private boolean keepAlive;
@Autowired
private MqttTransportContext context;
@ -69,7 +72,8 @@ public class MqttTransportService {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(context));
.childHandler(new MqttTransportServerInitializer(context))
.childOption(ChannelOption.SO_KEEPALIVE, keepAlive);
serverChannel = b.bind(host, port).sync().channel();
log.info("Mqtt transport started!");

10
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java

@ -172,8 +172,13 @@ public class GatewaySessionHandler {
if (!deviceEntry.getValue().isJsonArray()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray());
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg));
try {
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray());
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg));
} catch (Throwable e) {
UUID gatewayId = new UUID(gateway.getDeviceIdMSB(), gateway.getDeviceIdLSB());
log.warn("[{}][{}] Failed to convert telemetry: {}", gatewayId, deviceName, deviceEntry.getValue(), e);
}
}
@Override
@ -204,7 +209,6 @@ public class GatewaySessionHandler {
TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject());
transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg));
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t);

12
dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java

@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EventId;
@ -69,6 +70,9 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
@Value("${cassandra.query.events_ttl:0}")
private int eventsTtl;
@Value("${cassandra.query.debug_events_ttl}")
private int debugEventsTtl;
@Override
public Event save(TenantId tenantId, Event event) {
try {
@ -188,11 +192,15 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
.value(ModelConstants.EVENT_TYPE_PROPERTY, entity.getEventType())
.value(ModelConstants.EVENT_UID_PROPERTY, entity.getEventUid())
.value(ModelConstants.EVENT_BODY_PROPERTY, entity.getBody());
if (ifNotExists) {
insert = insert.ifNotExists();
}
if(ttl > 0){
insert.using(ttl(ttl));
int selectedTtl = entity.getEventType().equals(DataConstants.DEBUG_RULE_NODE) ? debugEventsTtl : ttl;
if(selectedTtl > 0){
insert.using(ttl(selectedTtl));
}
ResultSetFuture resultSetFuture = executeAsyncWrite(tenantId, insert);
return Futures.transform(resultSetFuture, rs -> {

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

@ -48,6 +48,8 @@ cassandra.query.ts_key_value_partitioning=HOURS
cassandra.query.ts_key_value_ttl=0
cassandra.query.debug_events_ttl=604800
cassandra.query.max_limit_per_request=1000
cassandra.query.buffer_size=100000
cassandra.query.concurrent_limit=1000

2
pom.xml

@ -47,7 +47,7 @@
<guava.version>21.0</guava.version>
<caffeine.version>2.6.1</caffeine.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-validator.version>1.5.0</commons-validator.version>
<commons-validator.version>1.6</commons-validator.version>
<commons-io.version>2.5</commons-io.version>
<commons-csv.version>1.4</commons-csv.version>
<jackson.version>2.9.8</jackson.version>

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java

@ -65,7 +65,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
final Alarm msgAlarm;
if (!config.isUseMessageAlarmData()) {
alarmType = config.getAlarmType();
alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg.getMetaData());
msgAlarm = null;
} else {
try {
@ -138,7 +138,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
.status(AlarmStatus.ACTIVE_UNACK)
.severity(config.getSeverity())
.propagate(config.isPropagate())
.type(config.getAlarmType())
.type(TbNodeUtils.processPattern(this.config.getAlarmType(), msg.getMetaData()))
//todo-vp: alarm date should be taken from Message or current Time should be used?
// .startTs(System.currentTimeMillis())
// .endTs(System.currentTimeMillis())

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;
}
});
}
}

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

1
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -34,6 +34,7 @@ transport:
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
so_keep_alive: "${NETTY_SO_KEEPALIVE:false}"
# MQTT SSL configuration
ssl:
# Enable/disable SSL support

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)"

4
ui/src/app/locale/locale.constant-de_DE.json

@ -23,7 +23,7 @@
"remove": "Löschen",
"search": "Suche",
"clear-search": "Suchanfrage löschen",
"assign": "Zoordnen",
"assign": "Zuordnen",
"unassign": "Zuordnung aufheben",
"share": "Teilen",
"make-private": "Privat machen",
@ -1640,4 +1640,4 @@
"uk_UA": "Ukrainisch"
}
}
}
}

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": "Показати мінімальне значення ",

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

@ -740,6 +740,102 @@
"no-entities-prompt": "没有找到实体",
"no-data": "无数据"
},
"entity-view": {
"entity-view": "实体视图",
"entity-view-required": "实体视图必填。",
"entity-views": "实体视图",
"management": "实体视图管理",
"view-entity-views": "查看实体视图",
"entity-view-alias": "实体视图别名",
"aliases": "实体视图别名",
"no-alias-matching": "'{{alias}}' 没有找到。",
"no-aliases-found": "找不到别名。",
"no-key-matching": "'{{key}}' 没有找到。",
"no-keys-found": "找不到密钥。",
"create-new-alias": "创建一个新的别名!",
"create-new-key": "创建一个新的密钥!",
"duplicate-alias-error": "找到重复别名 '{{alias}}'.<br>实体视图别名在仪表板中必须是唯一的。",
"configure-alias": "配置 '{{alias}}' 别名",
"no-entity-views-matching": "没有实体视图匹配 '{{entity}}' 被找到。",
"alias": "别名",
"alias-required": "视图实体别名必填。",
"remove-alias": "移除视图实体别名",
"add-alias": "添加视图实体别名",
"name-starts-with": "实体视图名称前缀",
"entity-view-list": "实体视图列表",
"use-entity-view-name-filter": "使用过滤器",
"entity-view-list-empty": "未选择任何实体视图。",
"entity-view-name-filter-required": "实体视图名称过滤器必填。",
"entity-view-name-filter-no-entity-view-matched": "没有实体视图名称前缀 '{{entityView}}' 被找到。",
"add": "添加视图实体",
"assign-to-customer": "分配给客户",
"assign-entity-view-to-customer": "将实体视图分配给客户",
"assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图",
"no-entity-views-text": "找不到实体视图",
"assign-to-customer-text": "请选择客户以分配实体视图",
"entity-view-details": "实体视图详细信息",
"add-entity-view-text": "添加新的实体视图",
"delete": "删除实体视图",
"assign-entity-views": "分配实体视图",
"assign-entity-views-text": "分配 { count, plural, 1 {1 实体视图} other {# 实体视图} } 给客户",
"delete-entity-views": "移除实体视图",
"unassign-from-customer": "取消分配客户",
"unassign-entity-views": "取消分配实体视图",
"unassign-entity-views-action-title": "从客户处取消分配 { count, plural, 1 {1 实体视图} other {# 实体视图} }",
"assign-new-entity-view": "分配新的实体视图",
"delete-entity-view-title": "您确定要删除实体视图 '{{entityViewName}}'吗?",
"delete-entity-view-text": "请注意,在确认后实体视图和所有相关数据将变得不可恢复。",
"delete-entity-views-title": "你确定要删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }吗?",
"delete-entity-views-action-title": "删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }",
"delete-entity-views-text": "请注意,在确认后将删除所有选定的实体视图,并且所有相关数据将变为不可恢复。",
"unassign-entity-view-title": "您确定要取消分配实体视图 '{{entityViewName}}'吗?",
"unassign-entity-view-text": "确认后,实体视图将被取消分配,客户将无法访问。",
"unassign-entity-view": "取消分配实体视图",
"unassign-entity-views-title": "你确定要取消分配 { count, plural, 1 {1 实体视图} other {# 实体视图} }吗?",
"unassign-entity-views-text": "确认后,所有选定的实体视图都将被取消分配,客户将无法访问。",
"entity-view-type": "实体视图类型",
"entity-view-type-required": "实体视图类型必填。",
"select-entity-view-type": "选择实体视图类型",
"enter-entity-view-type": "输入实体视图类型",
"any-entity-view": "任何实体视图",
"no-entity-view-types-matching": "没有找到匹配 '{{entitySubtype}}' 的实体视图类型。",
"entity-view-type-list-empty": "未选择任何实体视图类型。",
"entity-view-types": "实体视图类型",
"name": "名称",
"name-required": "名称必填。",
"description": "描述",
"events": "事件",
"details": "详情",
"copyId": "复制实体视图ID",
"assignedToCustomer": "分配给客户",
"unable-entity-view-device-alias-title": "无法删除实体视图别名",
"unable-entity-view-device-alias-text": "设备别名 '{{entityViewAlias}}' 无法删除,因为它由以下小部件使用:<br/>{{widgetsList}}",
"select-entity-view": "选择实体视图",
"make-public": "将实体视图公开",
"make-private": "将实体视图设为私有",
"start-date": "开始日期",
"start-ts": "开始时间",
"end-date": "结束日期",
"end-ts": "结束时间",
"date-limits": "日期范围",
"client-attributes": "客户属性",
"shared-attributes": "共享属性",
"server-attributes": "服务器属性",
"timeseries": "时间序列",
"client-attributes-placeholder": "客户属性",
"shared-attributes-placeholder": "共享属性",
"server-attributes-placeholder": "服务器属性",
"timeseries-placeholder": "时间序列",
"target-entity": "目标实体",
"attributes-propagation": "属性传播",
"attributes-propagation-hint": "每次保存或更新此实体视图时,实体视图将自动从目标实体复制指定的属性。出于性能原因,目标实体属性不会在每次属性更改时传播到实体视图。通过在规则链中配置“copy to view”规则节点,并将“Post attributes”和“Attributes Updated”消息链接到新的规则节点,可以启用自动传播。",
"timeseries-data": "时间序列数据",
"timeseries-data-hint": "配置实体视图可访问目标实体的时间序列数据键。此时间序列数据是只读的。",
"make-public-entity-view-title": "您确定要将实体视图 '{{entityViewName}}' 设为公开吗?",
"make-public-entity-view-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。",
"make-private-entity-view-title": "您确定要将实体视图 '{{entityViewName}}' 设为私有吗?",
"make-private-entity-view-text": "确认后,设备及其所有数据将被设为私有,不被其他人访问。"
},
"event": {
"event-type": "事件类型",
"type-error": "错误",
@ -1442,7 +1538,7 @@
"Dec": "12月",
"January": "一月",
"February": "二月",
"March": "游行",
"March": "三月",
"April": "四月",
"June": "六月",
"July": "七月",
@ -1472,7 +1568,7 @@
"6 months": "6个月",
"Custom interval": "自定义间隔",
"Interval": "间隔",
"Step size": "一步的大小",
"Step size": "步长",
"Ok": "Ok"
}
}
@ -1509,4 +1605,4 @@
"uk_UA": "乌克兰"
}
}
}
}

4
ui/src/app/widget/lib/google-map.js

@ -19,7 +19,7 @@ var gmGlobals = {
}
export default class TbGoogleMap {
constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, gmApiKey, gmDefaultMapType) {
constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, gmApiKey, gmDefaultMapType) {
var tbMap = this;
this.utils = utils;
@ -44,7 +44,7 @@ export default class TbGoogleMap {
function initGoogleMap() {
tbMap.map = new google.maps.Map($containerElement[0], { // eslint-disable-line no-undef
scrollwheel: true,
scrollwheel: !disableScrollZooming,
mapTypeId: getGoogleMapTypeId(tbMap.defaultMapType),
zoom: tbMap.defaultZoomLevel || 8
});

4
ui/src/app/widget/lib/image-map.js

@ -20,7 +20,7 @@ const maxZoom = 4;
export default class TbImageMap {
constructor(ctx, $containerElement, utils, initCallback, imageUrl, posFunction, imageEntityAlias, imageUrlAttribute) {
constructor(ctx, $containerElement, utils, initCallback, imageUrl, disableScrollZooming, posFunction, imageEntityAlias, imageUrlAttribute) {
this.ctx = ctx;
this.utils = utils;
@ -34,6 +34,7 @@ export default class TbImageMap {
this.height = 0;
this.markers = [];
this.initCallback = initCallback;
this.disableScrollZooming = disableScrollZooming;
if (angular.isDefined(posFunction) && posFunction.length > 0) {
try {
@ -165,6 +166,7 @@ export default class TbImageMap {
this.map = L.map(this.$containerElement[0], {
minZoom: 1,
maxZoom: maxZoom,
scrollWheelZoom: !this.disableScrollZooming,
center: center,
zoom: 1,
crs: L.CRS.Simple,

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

@ -78,18 +78,29 @@ export default class TbMapWidgetV2 {
tbMap.tooltipActionsMap[descriptor.name] = descriptor;
});
let openStreetMapProvider = {};
if (mapProvider === 'google-map') {
this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
} else if (mapProvider === 'openstreet-map') {
this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.mapProvider);
if (settings.useCustomProvider && settings.customProviderTileUrl) {
openStreetMapProvider.name = settings.customProviderTileUrl;
openStreetMapProvider.isCustom = true;
} else {
openStreetMapProvider.name = settings.mapProvider;
}
this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider);
} else if (mapProvider === 'here') {
openStreetMapProvider.name = settings.mapProvider;
this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider, settings.credentials);
} else if (mapProvider === 'image-map') {
this.map = new TbImageMap(this.ctx, $element, this.utils, initCallback,
settings.mapImageUrl,
settings.disableScrollZooming,
settings.posFunction,
settings.imageEntityAlias,
settings.imageUrlAttribute);
} else if (mapProvider === 'tencent-map') {
this.map = new TbTencentMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType);
this.map = new TbTencentMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType);
}
@ -663,6 +674,8 @@ export default class TbMapWidgetV2 {
return imageMapSettingsSchema;
} else if (mapProvider === 'tencent-map') {
schema = angular.copy(tencentMapSettingsSchema);
} else if (mapProvider === 'here') {
schema = angular.copy(hereMapSettingsSchema);
}
angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties);
schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required);
@ -790,6 +803,62 @@ const tencentMapSettingsSchema =
]
};
const hereMapSettingsSchema =
{
"schema": {
"title": "HERE Map Configuration",
"type": "object",
"properties": {
"mapProvider": {
"title": "Map layer",
"type": "string",
"default": "HERE.normalDay"
},
"credentials":{
"type": "object",
"properties": {
"app_id": {
"title": "HERE app id",
"type": "string"
},
"app_code": {
"title": "HERE app code",
"type": "string"
}
},
"required": ["app_id", "app_code"]
}
},
"required": []
},
"form": [
{
"key": "mapProvider",
"type": "rc-select",
"multiple": false,
"items": [
{
"value": "HERE.normalDay",
"label": "HERE.normalDay (Default)"
},
{
"value": "HERE.normalNight",
"label": "HERE.normalNight"
},
{
"value": "HERE.hybridDay",
"label": "HERE.hybridDay"
},
{
"value": "HERE.terrainDay",
"label": "HERE.terrainDay"
}
]
},
"credentials"
]
};
const openstreetMapSettingsSchema =
{
"schema": {
@ -800,7 +869,17 @@ const openstreetMapSettingsSchema =
"title": "Map provider",
"type": "string",
"default": "OpenStreetMap.Mapnik"
}
},
"useCustomProvider": {
"title": "Use custom provider",
"type": "boolean",
"default": false
},
"customProviderTileUrl": {
"title": "Custom provider tile URL",
"type": "string",
"default": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
}
},
"required": []
},
@ -839,7 +918,9 @@ const openstreetMapSettingsSchema =
"label": "CartoDB.DarkMatter"
}
]
}
},
"useCustomProvider",
"customProviderTileUrl"
]
};
@ -858,6 +939,11 @@ const commonMapSettingsSchema =
"type": "boolean",
"default": true
},
"disableScrollZooming": {
"title": "Disable scroll zooming",
"type": "boolean",
"default": false
},
"latKeyName": {
"title": "Latitude key name",
"type": "string",
@ -998,6 +1084,7 @@ const commonMapSettingsSchema =
"form": [
"defaultZoomLevel",
"fitMapBounds",
"disableScrollZooming",
"latKeyName",
"lngKeyName",
"showLabel",
@ -1109,6 +1196,11 @@ const imageMapSettingsSchema =
"type": "string",
"default": ""
},
"disableScrollZooming": {
"title": "Disable scroll zooming",
"type": "boolean",
"default": false
},
"xPosKeyName": {
"title": "X position key name",
"type": "string",
@ -1226,6 +1318,7 @@ const imageMapSettingsSchema =
},
"imageEntityAlias",
"imageUrlAttribute",
"disableScrollZooming",
"xPosKeyName",
"yPosKeyName",
"showLabel",

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

@ -19,7 +19,7 @@ import 'leaflet-providers';
export default class TbOpenStreetMap {
constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, mapProvider) {
constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, mapProvider, credentials) {
this.utils = utils;
this.defaultZoomLevel = defaultZoomLevel;
@ -28,13 +28,23 @@ export default class TbOpenStreetMap {
this.tooltips = [];
if (!mapProvider) {
mapProvider = "OpenStreetMap.Mapnik";
mapProvider = {
name: "OpenStreetMap.Mapnik"
};
}
if (mapProvider.name.startsWith("HERE.")) {
credentials.app_id = credentials.app_id || "AhM6TzD9ThyK78CT3ptx";
credentials.app_code = credentials.app_code || "p6NPiITB3Vv0GMUFnkLOOg";
}
this.map = L.map($containerElement[0]).setView([0, 0], this.defaultZoomLevel || 8);
var tileLayer = L.tileLayer.provider(mapProvider);
if (disableScrollZooming) {
this.map.scrollWheelZoom.disable();
}
var tileLayer = mapProvider.isCustom ? L.tileLayer(mapProvider.name) : L.tileLayer.provider(mapProvider.name, credentials);
tileLayer.addTo(this.map);
if (initCallback) {

4
ui/src/app/widget/lib/tencent-map.js

@ -19,7 +19,7 @@ var tmGlobals = {
}
export default class TbTencentMap {
constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, tmApiKey, tmDefaultMapType) {
constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, tmApiKey, tmDefaultMapType) {
var tbMap = this;
this.utils = utils;
this.defaultZoomLevel = defaultZoomLevel;
@ -42,7 +42,7 @@ export default class TbTencentMap {
function initTencentMap() {
tbMap.map = new qq.maps.Map($containerElement[0], { // eslint-disable-line no-undef
scrollwheel: true,
scrollwheel: !disableScrollZooming,
mapTypeId: getTencentMapTypeId(tbMap.defaultMapType),
zoom: tbMap.defaultZoomLevel || 8
});

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

@ -286,7 +286,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
}
}
vm.dontFitMapBounds = vm.ctx.settings.fitMapBounds === false;
vm.map = new TbOpenStreetMap(vm.ctx.$element, vm.utils, initCallback, vm.defaultZoomLevel, vm.dontFitMapBounds, null, vm.staticSettings.mapProvider);
vm.map = new TbOpenStreetMap(vm.ctx.$element, vm.utils, initCallback, vm.defaultZoomLevel, vm.dontFitMapBounds, vm.staticSettings.disableScrollZooming, null, vm.staticSettings.mapProvider);
vm.map.bounds = vm.map.createBounds();
vm.map.invalidateSize(true);
vm.map.bounds = vm.map.createBounds();
@ -307,7 +307,8 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
staticSettings.disabledButtonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.3).toRgbString();
staticSettings.polygonColor = tinycolor(vm.ctx.settings.polygonColor).toHexString();
staticSettings.polygonStrokeColor = tinycolor(vm.ctx.settings.polygonStrokeColor).toHexString();
staticSettings.mapProvider = vm.ctx.settings.mapProvider || "OpenStreetMap.Mapnik";
staticSettings.mapProvider = vm.ctx.settings.mapProvider ? {name: vm.ctx.settings.mapProvider} : {name: "OpenStreetMap.Mapnik"};
staticSettings.disableScrollZooming = vm.ctx.settings.disableScrollZooming || false;
staticSettings.latKeyName = vm.ctx.settings.latKeyName || "latitude";
staticSettings.lngKeyName = vm.ctx.settings.lngKeyName || "longitude";
staticSettings.polKeyName = vm.ctx.settings.polKeyName || "coordinates";
@ -362,6 +363,12 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
iconSize: [30, 30],
iconAnchor: [15, 15]
});
if (vm.ctx.settings.useCustomProvider && vm.ctx.settings.customProviderTileUrl) {
staticSettings.mapProvider.name = vm.ctx.settings.customProviderTileUrl;
staticSettings.mapProvider.isCustom = true;
}
if (angular.isDefined(vm.ctx.settings.markerImage)) {
staticSettings.icon = L.icon({
iconUrl: vm.ctx.settings.markerImage,

Loading…
Cancel
Save