thingsboard-aplcache

Changes

docker/README.md 42(+21 -21)

pom.xml 18(+12 -6)

tools/pom.xml 1(+0 -1)

Details

diff --git a/application/pom.xml b/application/pom.xml
index 6280d00..bc0856b 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -42,6 +42,10 @@
 
     <dependencies>
         <dependency>
+            <groupId>de.ruedigermoeller</groupId>
+            <artifactId>fst</artifactId>
+        </dependency>
+        <dependency>
             <groupId>io.netty</groupId>
             <artifactId>netty-transport-native-epoll</artifactId>
             <version>${netty.version}</version>
@@ -264,6 +268,10 @@
             <groupId>org.javadelight</groupId>
             <artifactId>delight-nashorn-sandbox</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.springfox.ui</groupId>
+            <artifactId>springfox-swagger-ui-rfc6570</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.cql b/application/src/main/data/upgrade/2.1.1/schema_update.cql
index 36ac8e4..73793c4 100644
--- a/application/src/main/data/upgrade/2.1.1/schema_update.cql
+++ b/application/src/main/data/upgrade/2.1.1/schema_update.cql
@@ -14,13 +14,6 @@
 -- limitations under the License.
 --
 
-DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_name;
-DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_search_text;
-DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_customer;
-DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_entity_id;
-
-DROP TABLE IF EXISTS thingsboard.entity_views;
-
 CREATE TABLE IF NOT EXISTS thingsboard.entity_views (
     id timeuuid,
     entity_id timeuuid,
diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.sql b/application/src/main/data/upgrade/2.1.1/schema_update.sql
index bd2c341..ecf90e0 100644
--- a/application/src/main/data/upgrade/2.1.1/schema_update.sql
+++ b/application/src/main/data/upgrade/2.1.1/schema_update.sql
@@ -14,10 +14,8 @@
 -- limitations under the License.
 --
 
-DROP TABLE IF EXISTS entity_views;
-
 CREATE TABLE IF NOT EXISTS entity_views (
-    id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY,
+    id varchar(31) NOT NULL CONSTRAINT entity_views_pkey PRIMARY KEY,
     entity_id varchar(31),
     entity_type varchar(255),
     tenant_id varchar(31),
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index e46a959..c0030cb 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.actors.device;
 
 import akka.actor.ActorContext;
-import akka.event.LoggingAdapter;
 import com.datastax.driver.core.utils.UUIDs;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
@@ -26,12 +25,12 @@ import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
 import com.google.protobuf.InvalidProtocolBufferException;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
 import org.thingsboard.rule.engine.api.RpcError;
 import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
 import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
-import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -42,7 +41,6 @@ import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
-import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
 import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
@@ -81,12 +79,14 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
+import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
+import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
+
 /**
  * @author Andrew Shvayka
  */
@@ -263,10 +263,8 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
     }
 
     private void handleGetAttributesRequest(ActorContext context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
-        ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.CLIENT_SCOPE, toOptionalSet(request.getClientAttributeNamesList()));
-        ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.SHARED_SCOPE, toOptionalSet(request.getSharedAttributeNamesList()));
         int requestId = request.getRequestId();
-        Futures.addCallback(Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)), new FutureCallback<List<List<AttributeKvEntry>>>() {
+        Futures.addCallback(getAttributesKvEntries(request), new FutureCallback<List<List<AttributeKvEntry>>>() {
             @Override
             public void onSuccess(@Nullable List<List<AttributeKvEntry>> result) {
                 GetAttributeResponseMsg responseMsg = GetAttributeResponseMsg.newBuilder()
@@ -287,16 +285,35 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
         });
     }
 
-    private ListenableFuture<List<AttributeKvEntry>> getAttributeKvEntries(DeviceId deviceId, String scope, Optional<Set<String>> names) {
-        if (names.isPresent()) {
-            if (!names.get().isEmpty()) {
-                return systemContext.getAttributesService().find(tenantId, deviceId, scope, names.get());
-            } else {
-                return systemContext.getAttributesService().findAll(tenantId, deviceId, scope);
-            }
+    private ListenableFuture<List<List<AttributeKvEntry>>> getAttributesKvEntries(GetAttributeRequestMsg request) {
+        ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture;
+        ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture;
+        if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
+            clientAttributesFuture = findAllAttributesByScope(CLIENT_SCOPE);
+            sharedAttributesFuture = findAllAttributesByScope(SHARED_SCOPE);
+        } else if (!CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
+            clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
+            sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
+        } else if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
+            clientAttributesFuture = Futures.immediateFuture(Collections.emptyList());
+            sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
         } else {
-            return Futures.immediateFuture(Collections.emptyList());
+            sharedAttributesFuture = Futures.immediateFuture(Collections.emptyList());
+            clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
         }
+        return Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture));
+    }
+
+    private ListenableFuture<List<AttributeKvEntry>> findAllAttributesByScope(String scope) {
+        return systemContext.getAttributesService().findAll(tenantId, deviceId, scope);
+    }
+
+    private ListenableFuture<List<AttributeKvEntry>> findAttributesByScope(Set<String> attributesSet, String scope) {
+        return systemContext.getAttributesService().find(tenantId, deviceId, scope, attributesSet);
+    }
+
+    private Set<String> toSet(List<String> strings) {
+        return new HashSet<>(strings);
     }
 
     private void handlePostAttributesRequest(ActorContext context, SessionInfoProto sessionInfo, PostAttributeMsg postAttributes) {
@@ -368,7 +385,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
             AttributeUpdateNotificationMsg.Builder notification = AttributeUpdateNotificationMsg.newBuilder();
             if (msg.isDeleted()) {
                 List<String> sharedKeys = msg.getDeletedKeys().stream()
-                        .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
+                        .filter(key -> SHARED_SCOPE.equals(key.getScope()))
                         .map(AttributeKey::getAttributeKey)
                         .collect(Collectors.toList());
                 if (!sharedKeys.isEmpty()) {
@@ -376,7 +393,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
                     hasNotificationData = true;
                 }
             } else {
-                if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) {
+                if (SHARED_SCOPE.equals(msg.getScope())) {
                     List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues());
                     if (attributes.size() > 0) {
                         List<TsKvProto> sharedUpdated = msg.getValues().stream().map(this::toTsKvProto)
@@ -545,14 +562,6 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
         return json;
     }
 
-    private Optional<Set<String>> toOptionalSet(List<String> strings) {
-        if (strings == null || strings.isEmpty()) {
-            return Optional.empty();
-        } else {
-            return Optional.of(new HashSet<>(strings));
-        }
-    }
-
     private void sendToTransport(GetAttributeResponseMsg responseMsg, SessionInfoProto sessionInfo) {
         DeviceActorToTransportMsg msg = DeviceActorToTransportMsg.newBuilder()
                 .setSessionIdMSB(sessionInfo.getSessionIdMSB())
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
index 7f200e1..452b9d7 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.actors.ruleChain;
 
 import akka.actor.ActorContext;
 import akka.actor.ActorRef;
-import akka.event.LoggingAdapter;
 import org.thingsboard.rule.engine.api.TbContext;
 import org.thingsboard.rule.engine.api.TbNode;
 import org.thingsboard.rule.engine.api.TbNodeConfiguration;
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
index 1f084e1..6a53fd3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
@@ -16,9 +16,6 @@
 package org.thingsboard.server.actors.service;
 
 import akka.actor.ActorRef;
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
-import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
 import org.thingsboard.server.actors.stats.StatsPersistMsg;
@@ -60,7 +57,8 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
                 scheduleStatsPersistTick();
             }
         } catch (Exception e) {
-            log.warn("[{}][{}] Failed to start {} processor: {}", tenantId, id, id.getEntityType(), e);
+            log.warn("[{}][{}] Failed to start {} processor.", tenantId, id, id.getEntityType());
+            log.warn("Error:", e);
             logAndPersist("OnStart", e, true);
             logLifecycleEvent(ComponentLifecycleEvent.STARTED, e);
         }
@@ -149,10 +147,13 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
 
     private void logAndPersist(String method, Exception e, boolean critical) {
         errorsOccurred++;
+        String componentName = processor != null ? processor.getComponentName() : "Unknown";
         if (critical) {
-            log.warn("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, processor.getComponentName(), method, e);
+            log.warn("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, componentName, method);
+            log.warn("Critical Error: ", e);
         } else {
-            log.debug("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, processor.getComponentName(), method, e);
+            log.debug("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, componentName, method);
+            log.debug("Debug Error: ", e);
         }
         long ts = System.currentTimeMillis();
         if (ts - lastPersistedErrorTs > getErrorPersistFrequency()) {
diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
index 2254cf3..52eef82 100644
--- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
@@ -64,7 +64,8 @@ public class SwaggerConfiguration {
                     .paths(apiPaths())
                     .build()
                     .securitySchemes(newArrayList(jwtTokenKey()))
-                    .securityContexts(newArrayList(securityContext()));
+                    .securityContexts(newArrayList(securityContext()))
+                    .enableUrlTemplating(true);
       }
 
       private ApiKey jwtTokenKey() {
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index 6261e74..68cf2ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -93,6 +93,19 @@ public class AlarmController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE)
+    @ResponseBody
+    public Boolean deleteAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
+        checkParameter(ALARM_ID, strAlarmId);
+        try {
+            AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
+            return alarmService.deleteAlarm(getTenantId(), alarmId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST)
     @ResponseStatus(value = HttpStatus.OK)
     public void ackAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
index bebd2cc..4748c22 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -279,8 +279,14 @@ public class TelemetryController extends BaseController {
             deleteFromTs = 0L;
             deleteToTs = System.currentTimeMillis();
         } else {
-            deleteFromTs = startTs;
-            deleteToTs = endTs;
+            if (startTs == null || endTs == null) {
+                deleteToTs = endTs;
+                return getImmediateDeferredResult("When deleteAllDataForKeys is false, start and end timestamp values shouldn't be empty", HttpStatus.BAD_REQUEST);
+            }
+            else{
+                deleteFromTs = startTs;
+                deleteToTs = endTs;
+            }
         }
 
         return accessValidator.validateEntityAndCallback(user, entityIdStr, (result, tenantId, entityId) -> {
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportApiService.java
index 7436de8..2ad5e37 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportApiService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportApiService.java
@@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -100,7 +101,7 @@ public class LocalTransportApiService implements TransportApiService {
         //TODO: Make async and enable caching
         DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsId);
         if (credentials != null && credentials.getCredentialsType() == credentialsType) {
-            return getDeviceInfo(credentials.getDeviceId());
+            return getDeviceInfo(credentials.getDeviceId(), credentials);
         } else {
             return getEmptyTransportApiResponseFuture();
         }
@@ -135,15 +136,20 @@ public class LocalTransportApiService implements TransportApiService {
     }
 
 
-    private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId) {
+    private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) {
         return Futures.transform(deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, deviceId), device -> {
             if (device == null) {
                 log.trace("[{}] Failed to lookup device by id", deviceId);
                 return getEmptyTransportApiResponse();
             }
             try {
+                ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder();
+                builder.setDeviceInfo(getDeviceInfoProto(device));
+                if(!StringUtils.isEmpty(credentials.getCredentialsValue())){
+                    builder.setCredentialsBody(credentials.getCredentialsValue());
+                }
                 return TransportApiResponseMsg.newBuilder()
-                        .setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.newBuilder().setDeviceInfo(getDeviceInfoProto(device)).build()).build();
+                        .setValidateTokenResponseMsg(builder.build()).build();
             } catch (JsonProcessingException e) {
                 log.warn("[{}] Failed to lookup device by id", deviceId, e);
                 return getEmptyTransportApiResponse();
diff --git a/application/src/main/scripts/install/install_dev_db.sh b/application/src/main/scripts/install/install_dev_db.sh
index 212a2ac..70169e0 100644
--- a/application/src/main/scripts/install/install_dev_db.sh
+++ b/application/src/main/scripts/install/install_dev_db.sh
@@ -28,7 +28,7 @@ export LOADER_PATH=${BASE}/conf,${BASE}/extensions
 export SQL_DATA_FOLDER=${SQL_DATA_FOLDER:-/tmp}
 
 
-run_user=thingsboard
+run_user="$USER"
 
 sudo -u "$run_user" -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
                     -Dinstall.data_dir=${installDir} \
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
index aa643eb..29e5859 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
@@ -54,18 +54,18 @@ import java.util.concurrent.TimeUnit;
 @Slf4j
 @Component("MqttSslHandlerProvider")
 @ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.http.enabled}'=='true')")
-@ConditionalOnProperty(prefix = "mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
+@ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class MqttSslHandlerProvider {
 
-    @Value("${mqtt.ssl.protocol}")
+    @Value("${transport.mqtt.ssl.protocol}")
     private String sslProtocol;
-    @Value("${mqtt.ssl.key_store}")
+    @Value("${transport.mqtt.ssl.key_store}")
     private String keyStoreFile;
-    @Value("${mqtt.ssl.key_store_password}")
+    @Value("${transport.mqtt.ssl.key_store_password}")
     private String keyStorePassword;
-    @Value("${mqtt.ssl.key_password}")
+    @Value("${transport.mqtt.ssl.key_password}")
     private String keyPassword;
-    @Value("${mqtt.ssl.key_store_type}")
+    @Value("${transport.mqtt.ssl.key_store_type}")
     private String keyStoreType;
 
     @Autowired
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 3986837..1a759bc 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -271,6 +271,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
                     case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC:
                     case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC:
                     case MqttTopics.GATEWAY_RPC_TOPIC:
+                    case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC:
                     case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC:
                         registerSubQoS(topic, grantedQoSList, reqQoS);
                         break;
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
index 5508378..eb8e5fb 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
@@ -36,9 +36,8 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
     @Override
     public void initChannel(SocketChannel ch) {
         ChannelPipeline pipeline = ch.pipeline();
-        SslHandler sslHandler = null;
         if (context.getSslHandlerProvider() != null) {
-            sslHandler = context.getSslHandlerProvider().getSslHandler();
+            SslHandler sslHandler = context.getSslHandlerProvider().getSslHandler();
             pipeline.addLast(sslHandler);
             context.setSslHandler(sslHandler);
         }
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
index 5a81b97..b5e5d74 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
@@ -40,7 +40,7 @@ public abstract class AbstractTransportService implements TransportService {
     private boolean rateLimitEnabled;
     @Value("${transport.rate_limits.tenant}")
     private String perTenantLimitsConf;
-    @Value("${transport.rate_limits.tenant}")
+    @Value("${transport.rate_limits.device}")
     private String perDevicesLimitsConf;
     @Value("${transport.sessions.inactivity_timeout}")
     private long sessionInactivityTimeout;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
index 5d5456f..68bbbe4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
@@ -31,6 +31,8 @@ import java.util.UUID;
  */
 public interface AlarmDao extends Dao<Alarm> {
 
+    Boolean deleteAlarm(TenantId tenantId, Alarm alarm);
+
     ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
 
     ListenableFuture<Alarm> findAlarmByIdAsync(TenantId tenantId, UUID key);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
index aace832..4cac094 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
@@ -35,6 +35,8 @@ public interface AlarmService {
 
     Alarm createOrUpdateAlarm(Alarm alarm);
 
+    Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId);
+
     ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
 
     ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long ackTs);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
index d0698b6..9405dd5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.alarm.Alarm;
 import org.thingsboard.server.common.data.alarm.AlarmId;
@@ -118,6 +119,21 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
         return alarmDao.findLatestByOriginatorAndType(tenantId, originator, type);
     }
 
+    @Override
+    public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) {
+        try {
+            log.debug("Deleting Alarm Id: {}", alarmId);
+            Alarm alarm = alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()).get();
+            if (alarm == null) {
+                return false;
+            }
+            deleteEntityRelations(tenantId, alarm.getId());
+            return alarmDao.deleteAlarm(tenantId, alarm);
+        } catch (ExecutionException | InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     private Alarm createAlarm(Alarm alarm) throws InterruptedException, ExecutionException {
         log.debug("New Alarm : {}", alarm);
         Alarm saved = alarmDao.save(alarm.getTenantId(), alarm);
@@ -127,9 +143,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
 
     private void createAlarmRelations(Alarm alarm) throws InterruptedException, ExecutionException {
         if (alarm.isPropagate()) {
-            EntityRelationsQuery query = new EntityRelationsQuery();
-            query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
-            List<EntityId> parentEntities = relationService.findByQuery(alarm.getTenantId(), query).get().stream().map(EntityRelation::getFrom).collect(Collectors.toList());
+            List<EntityId> parentEntities = getParentEntities(alarm);
             for (EntityId parentId : parentEntities) {
                 createAlarmRelation(alarm.getTenantId(), parentId, alarm.getId(), alarm.getStatus(), true);
             }
@@ -137,6 +151,12 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
         createAlarmRelation(alarm.getTenantId(), alarm.getOriginator(), alarm.getId(), alarm.getStatus(), true);
     }
 
+    private List<EntityId> getParentEntities(Alarm alarm) throws InterruptedException, ExecutionException {
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
+        return relationService.findByQuery(alarm.getTenantId(), query).get().stream().map(EntityRelation::getFrom).collect(Collectors.toList());
+    }
+
     private ListenableFuture<Alarm> updateAlarm(Alarm update) {
         alarmDataValidator.validate(update, Alarm::getTenantId);
         return getAndUpdate(update.getTenantId(), update.getId(), new Function<Alarm, Alarm>() {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
index ed8666f..48adbe3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.alarm;
 
+import com.datastax.driver.core.Statement;
 import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
 import com.google.common.util.concurrent.Futures;
@@ -79,6 +80,17 @@ public class CassandraAlarmDao extends CassandraAbstractModelDao<AlarmEntity, Al
     }
 
     @Override
+    public Boolean deleteAlarm(TenantId tenantId, Alarm alarm) {
+        Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, alarm.getId().getId()))
+            .and(eq(ALARM_TENANT_ID_PROPERTY, tenantId.getId()))
+            .and(eq(ALARM_ORIGINATOR_ID_PROPERTY, alarm.getOriginator().getId()))
+            .and(eq(ALARM_ORIGINATOR_TYPE_PROPERTY, alarm.getOriginator().getEntityType()))
+            .and(eq(ALARM_TYPE_PROPERTY, alarm.getType()));
+        log.debug("Remove request: {}", delete.toString());
+        return executeWrite(tenantId, delete).wasApplied();
+    }
+
+    @Override
     public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
         Select select = select().from(ALARM_COLUMN_FAMILY_NAME);
         Select.Where query = select.where();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
index a6d3ea6..348f21b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
@@ -52,22 +52,33 @@ public final class TsKvEntity implements ToData<TsKvEntry> {
     public TsKvEntity() {
     }
 
-    public TsKvEntity(Double avgLongValue, Double avgDoubleValue) {
-        if(avgLongValue != null) {
-            this.longValue = avgLongValue.longValue();
+    public TsKvEntity(Long longSumValue, Double doubleSumValue, Long longCountValue, Long doubleCountValue) {
+        double sum = 0.0;
+        if (longSumValue != null) {
+            sum += longSumValue;
         }
-        this.doubleValue = avgDoubleValue;
+        if (doubleSumValue != null) {
+            sum += doubleSumValue;
+        }
+        this.doubleValue = sum / (longCountValue + doubleCountValue);
     }
 
     public TsKvEntity(Long sumLongValue, Double sumDoubleValue) {
-        this.longValue = sumLongValue;
-        this.doubleValue = sumDoubleValue;
+        if (sumDoubleValue != null) {
+            this.doubleValue = sumDoubleValue + (sumLongValue != null ? sumLongValue.doubleValue() : 0.0);
+        } else {
+            this.longValue = sumLongValue;
+        }
     }
 
-    public TsKvEntity(String strValue, Long longValue, Double doubleValue) {
+    public TsKvEntity(String strValue, Long longValue, Double doubleValue, boolean max) {
         this.strValue = strValue;
-        this.longValue = longValue;
-        this.doubleValue = doubleValue;
+        if (longValue != null && doubleValue != null) {
+            this.doubleValue = max ? Math.max(doubleValue, longValue.doubleValue()) : Math.min(doubleValue, longValue.doubleValue());
+        } else {
+            this.longValue = longValue;
+            this.doubleValue = doubleValue;
+        }
     }
 
     public TsKvEntity(Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount) {
@@ -75,10 +86,8 @@ public final class TsKvEntity implements ToData<TsKvEntry> {
             this.longValue = booleanValueCount;
         } else if (strValueCount != 0) {
             this.longValue = strValueCount;
-        } else if (longValueCount != 0) {
-            this.longValue = longValueCount;
-        } else if (doubleValueCount != 0) {
-            this.longValue = doubleValueCount;
+        } else {
+            this.longValue = longValueCount + doubleValueCount;
         }
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
index 0c5690c..86e7188 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
@@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.id.EntityId;
@@ -274,14 +275,43 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
     public List<RuleNode> getRuleChainNodes(TenantId tenantId, RuleChainId ruleChainId) {
         Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
         List<EntityRelation> relations = getRuleChainToNodeRelations(tenantId, ruleChainId);
-        List<RuleNode> ruleNodes = relations.stream().map(relation -> ruleNodeDao.findById(tenantId, relation.getTo().getId())).collect(Collectors.toList());
+        List<RuleNode> ruleNodes = new ArrayList<>();
+        for (EntityRelation relation : relations) {
+            RuleNode ruleNode = ruleNodeDao.findById(tenantId, relation.getTo().getId());
+            if (ruleNode != null) {
+                ruleNodes.add(ruleNode);
+            } else {
+                relationService.deleteRelation(tenantId, relation);
+            }
+        }
         return ruleNodes;
     }
 
     @Override
     public List<EntityRelation> getRuleNodeRelations(TenantId tenantId, RuleNodeId ruleNodeId) {
         Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
-        return relationService.findByFrom(tenantId, ruleNodeId, RelationTypeGroup.RULE_NODE);
+        List<EntityRelation> relations = relationService.findByFrom(tenantId, ruleNodeId, RelationTypeGroup.RULE_NODE);
+        List<EntityRelation> validRelations = new ArrayList<>();
+        for (EntityRelation relation : relations) {
+            boolean valid = true;
+            EntityType toType = relation.getTo().getEntityType();
+            if (toType == EntityType.RULE_NODE || toType == EntityType.RULE_CHAIN) {
+                BaseData entity;
+                if (relation.getTo().getEntityType() == EntityType.RULE_NODE) {
+                    entity = ruleNodeDao.findById(tenantId, relation.getTo().getId());
+                } else {
+                    entity = ruleChainDao.findById(tenantId, relation.getTo().getId());
+                }
+                if (entity == null) {
+                    relationService.deleteRelation(tenantId, relation);
+                    valid = false;
+                }
+            }
+            if (valid) {
+                validRelations.add(relation);
+            }
+        }
+        return validRelations;
     }
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
index 2e071b1..3e2a80f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
@@ -69,6 +69,11 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
     }
 
     @Override
+    public Boolean deleteAlarm(TenantId tenantId, Alarm alarm) {
+        return removeById(tenantId, alarm.getUuidId());
+    }
+
+    @Override
     public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
         return service.submit(() -> {
             List<AlarmEntity> latest = alarmRepository.findLatestByOriginatorAndType(
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
index 296d173..92bdf7e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvRepository.java
@@ -55,7 +55,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
                 @Param("endTs") long endTs);
 
     @Async
-    @Query("SELECT new TsKvEntity(MAX(tskv.strValue), MAX(tskv.longValue), MAX(tskv.doubleValue)) FROM TsKvEntity tskv " +
+    @Query("SELECT new TsKvEntity(MAX(tskv.strValue), MAX(tskv.longValue), MAX(tskv.doubleValue), true) FROM TsKvEntity tskv " +
             "WHERE tskv.entityId = :entityId AND tskv.entityType = :entityType " +
             "AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts < :endTs")
     CompletableFuture<TsKvEntity> findMax(@Param("entityId") String entityId,
@@ -65,7 +65,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
                                           @Param("endTs") long endTs);
 
     @Async
-    @Query("SELECT new TsKvEntity(MIN(tskv.strValue), MIN(tskv.longValue), MIN(tskv.doubleValue)) FROM TsKvEntity tskv " +
+    @Query("SELECT new TsKvEntity(MIN(tskv.strValue), MIN(tskv.longValue), MIN(tskv.doubleValue), false) FROM TsKvEntity tskv " +
             "WHERE tskv.entityId = :entityId AND tskv.entityType = :entityType " +
             "AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts < :endTs")
     CompletableFuture<TsKvEntity> findMin(@Param("entityId") String entityId,
@@ -85,7 +85,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
                                             @Param("endTs") long endTs);
 
     @Async
-    @Query("SELECT new TsKvEntity(AVG(tskv.longValue), AVG(tskv.doubleValue)) FROM TsKvEntity tskv " +
+    @Query("SELECT new TsKvEntity(SUM(tskv.longValue), SUM(tskv.doubleValue), COUNT(tskv.longValue), COUNT(tskv.doubleValue)) FROM TsKvEntity tskv " +
             "WHERE tskv.entityId = :entityId AND tskv.entityType = :entityType " +
             "AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts < :endTs")
     CompletableFuture<TsKvEntity> findAvg(@Param("entityId") String entityId,
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
index b5ebb10..10e5c86 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
@@ -98,6 +98,10 @@ public class AggregatePartitionsFunction implements com.google.common.base.Funct
                 curLValue = getLongValue(row);
             }
             if (doubleCount > 0) {
+<<<<<<< HEAD
+=======
+                aggResult.hasDouble = true;
+>>>>>>> upstream/master
                 aggResult.dataType = DataType.DOUBLE;
                 curCount += doubleCount;
                 curDValue = getDoubleValue(row);
@@ -222,17 +226,36 @@ public class AggregatePartitionsFunction implements com.google.common.base.Funct
         if (aggResult.count == 0 || (aggResult.dataType == DataType.DOUBLE && aggResult.dValue == null) || (aggResult.dataType == DataType.LONG && aggResult.lValue == null)) {
             return Optional.empty();
         } else if (aggResult.dataType == DataType.DOUBLE || aggResult.dataType == DataType.LONG) {
+<<<<<<< HEAD
             double sum = Optional.ofNullable(aggResult.dValue).orElse(0.0d) + Optional.ofNullable(aggResult.lValue).orElse(0L);
             return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.SUM ? sum : (sum / aggResult.count))));
+=======
+            if(aggregation == Aggregation.AVG || aggResult.hasDouble) {
+                double sum = Optional.ofNullable(aggResult.dValue).orElse(0.0d) + Optional.ofNullable(aggResult.lValue).orElse(0L);
+                return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.SUM ? sum : (sum / aggResult.count))));
+            } else {
+                return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggregation == Aggregation.SUM ? aggResult.lValue : (aggResult.lValue / aggResult.count))));
+            }
+>>>>>>> upstream/master
         }
         return Optional.empty();
     }
 
     private Optional<TsKvEntry> processMinOrMaxResult(AggregationResult aggResult) {
         if (aggResult.dataType == DataType.DOUBLE || aggResult.dataType == DataType.LONG) {
+<<<<<<< HEAD
             double currentD = aggregation == Aggregation.MIN ? Optional.ofNullable(aggResult.dValue).orElse(Double.MAX_VALUE) : Optional.ofNullable(aggResult.dValue).orElse(Double.MIN_VALUE);
             double currentL = aggregation == Aggregation.MIN ? Optional.ofNullable(aggResult.lValue).orElse(Long.MAX_VALUE) : Optional.ofNullable(aggResult.lValue).orElse(Long.MIN_VALUE);
             return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.MIN ? Math.min(currentD, currentL) : Math.max(currentD, currentL))));
+=======
+            if(aggResult.hasDouble) {
+                double currentD = aggregation == Aggregation.MIN ? Optional.ofNullable(aggResult.dValue).orElse(Double.MAX_VALUE) : Optional.ofNullable(aggResult.dValue).orElse(Double.MIN_VALUE);
+                double currentL = aggregation == Aggregation.MIN ? Optional.ofNullable(aggResult.lValue).orElse(Long.MAX_VALUE) : Optional.ofNullable(aggResult.lValue).orElse(Long.MIN_VALUE);
+                return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.MIN ? Math.min(currentD, currentL) : Math.max(currentD, currentL))));
+            } else {
+                return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggResult.lValue)));
+            }
+>>>>>>> upstream/master
         }  else if (aggResult.dataType == DataType.STRING) {
             return Optional.of(new BasicTsKvEntry(ts, new StringDataEntry(key, aggResult.sValue)));
         } else {
@@ -247,5 +270,6 @@ public class AggregatePartitionsFunction implements com.google.common.base.Funct
         Double dValue = null;
         Long lValue = null;
         long count = 0;
+        boolean hasDouble = false;
     }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
index a5415f0..8a70059 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.util;
 
+import com.datastax.driver.core.*;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -22,6 +23,7 @@ import com.google.common.util.concurrent.SettableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.msg.tools.TbRateLimits;
+import org.thingsboard.server.dao.nosql.CassandraStatementTask;
 
 import javax.annotation.Nullable;
 import java.util.UUID;
@@ -183,12 +185,39 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
 
     private void logTask(String action, AsyncTaskContext<T, V> taskCtx) {
         if (log.isTraceEnabled()) {
-            log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx);
+            if (taskCtx.getTask() instanceof CassandraStatementTask) {
+                CassandraStatementTask cassStmtTask = (CassandraStatementTask) taskCtx.getTask();
+                if (cassStmtTask.getStatement() instanceof BoundStatement) {
+                    BoundStatement stmt = (BoundStatement) cassStmtTask.getStatement();
+                    String query = toStringWithValues(stmt, ProtocolVersion.V5);
+                    log.trace("[{}] {} task: {}, BoundStatement query: {}", taskCtx.getId(), action, taskCtx, query);
+                }
+            } else {
+                log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx);
+            }
         } else {
             log.debug("[{}] {} task", taskCtx.getId(), action);
         }
     }
 
+    private static String toStringWithValues(BoundStatement boundStatement, ProtocolVersion protocolVersion) {
+        CodecRegistry codecRegistry = boundStatement.preparedStatement().getCodecRegistry();
+        PreparedStatement preparedStatement = boundStatement.preparedStatement();
+        String query = preparedStatement.getQueryString();
+        ColumnDefinitions defs = preparedStatement.getVariables();
+        int index = 0;
+        for (ColumnDefinitions.Definition def : defs) {
+            DataType type = def.getType();
+            TypeCodec<Object> codec = codecRegistry.codecFor(type);
+            if (boundStatement.getBytesUnsafe(index) != null) {
+                Object value = codec.deserialize(boundStatement.getBytesUnsafe(index), protocolVersion);
+                query = query.replaceFirst("\\?", codec.format(value));
+            }
+            index++;
+        }
+        return query;
+    }
+
     protected int getQueueSize() {
         return queue.size();
     }
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 6d08611..7a6ef7b 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -72,7 +72,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv (
   long_v bigint,
   dbl_v double precision,
   last_update_ts bigint,
-  CONSTRAINT attribute_kv_unq_key UNIQUE (entity_type, entity_id, attribute_type, attribute_key)
+  CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_type, entity_id, attribute_type, attribute_key)
 );
 
 CREATE TABLE IF NOT EXISTS component_descriptor (
@@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS relation (
     relation_type_group varchar(255),
     relation_type varchar(255),
     additional_info varchar,
-    CONSTRAINT relation_unq_key UNIQUE (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
+    CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
 );
 
 CREATE TABLE IF NOT EXISTS tb_user (
diff --git a/dao/src/main/resources/sql/schema-ts.sql b/dao/src/main/resources/sql/schema-ts.sql
index 53bc15a..821e297 100644
--- a/dao/src/main/resources/sql/schema-ts.sql
+++ b/dao/src/main/resources/sql/schema-ts.sql
@@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS ts_kv (
     str_v varchar(10000000),
     long_v bigint,
     dbl_v double precision,
-    CONSTRAINT ts_kv_unq_key UNIQUE (entity_type, entity_id, key, ts)
+    CONSTRAINT ts_kv_pkey PRIMARY KEY (entity_type, entity_id, key, ts)
 );
 
 CREATE TABLE IF NOT EXISTS ts_kv_latest (
@@ -35,5 +35,5 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest (
     str_v varchar(10000000),
     long_v bigint,
     dbl_v double precision,
-    CONSTRAINT ts_kv_latest_unq_key UNIQUE (entity_type, entity_id, key)
+    CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_type, entity_id, key)
 );
diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
index 55c2f70..ddea70a 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
@@ -25,9 +25,7 @@ import java.util.Arrays;
 
 @RunWith(ClasspathSuite.class)
 @ClassnameFilters({
-        "org.thingsboard.server.dao.service.*ServiceNoSqlTest",
-        "org.thingsboard.server.dao.service.queue.cassandra.*.*.*Test",
-        "org.thingsboard.server.dao.service.queue.cassandra.*Test"
+        "org.thingsboard.server.dao.service.*ServiceNoSqlTest"
 })
 public class NoSqlDaoServiceTestSuite {
 
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
index 8c44374..50b2e7f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
@@ -31,7 +31,9 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 
+import java.util.List;
 import java.util.concurrent.ExecutionException;
 
 public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
@@ -184,4 +186,85 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
         Assert.assertEquals(1, alarms.getData().size());
         Assert.assertEquals(created, alarms.getData().get(0));
     }
+
+    @Test
+    public void testDeleteAlarm() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+
+        Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get());
+
+        long ts = System.currentTimeMillis();
+        Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId)
+                .type(TEST_ALARM)
+                .propagate(true)
+                .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK)
+                .startTs(ts).build();
+
+        Alarm created = alarmService.createOrUpdateAlarm(alarm);
+
+        TimePageData<AlarmInfo> alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        // Check parent relation
+        alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder()
+                .affectedEntityId(parentId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        List<EntityRelation> toAlarmRelations = relationService.findByTo(tenantId, created.getId(), RelationTypeGroup.ALARM);
+        Assert.assertEquals(8, toAlarmRelations.size());
+
+        List<EntityRelation> fromChildRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM);
+        Assert.assertEquals(4, fromChildRelations.size());
+
+        List<EntityRelation> fromParentRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM);
+        Assert.assertEquals(4, fromParentRelations.size());
+
+
+        Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId()));
+
+        Alarm fetched = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get();
+
+        Assert.assertNull("Alarm was returned when it was expected to be null", fetched);
+
+        alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(0, alarms.getData().size());
+
+        // Check parent relation
+        alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder()
+                .affectedEntityId(parentId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(0, alarms.getData().size());
+
+        toAlarmRelations = relationService.findByTo(tenantId, created.getId(), RelationTypeGroup.ALARM);
+        Assert.assertEquals(0, toAlarmRelations.size());
+
+        fromChildRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM);
+        Assert.assertEquals(0, fromChildRelations.size());
+
+        fromParentRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM);
+        Assert.assertEquals(0, fromParentRelations.size());
+
+    }
 }
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
index b409dea..7378bcd 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
@@ -221,13 +221,13 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
                 60000, 20000, 3, Aggregation.AVG))).get();
         assertEquals(3, list.size());
         assertEquals(10000, list.get(0).getTs());
-        assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue());
+        assertEquals(java.util.Optional.of(150.0), list.get(0).getDoubleValue());
 
         assertEquals(30000, list.get(1).getTs());
-        assertEquals(java.util.Optional.of(350L), list.get(1).getLongValue());
+        assertEquals(java.util.Optional.of(350.0), list.get(1).getDoubleValue());
 
         assertEquals(50000, list.get(2).getTs());
-        assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue());
+        assertEquals(java.util.Optional.of(550.0), list.get(2).getDoubleValue());
 
         list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
                 60000, 20000, 3, Aggregation.SUM))).get();
@@ -282,12 +282,110 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
         assertEquals(java.util.Optional.of(2L), list.get(2).getLongValue());
     }
 
+    @Test
+    public void testFindDeviceLongAndDoubleTsData() throws Exception {
+        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+        List<TsKvEntry> entries = new ArrayList<>();
+
+        entries.add(save(deviceId, 5000, 100));
+        entries.add(save(deviceId, 15000, 200.0));
+
+        entries.add(save(deviceId, 25000, 300));
+        entries.add(save(deviceId, 35000, 400.0));
+
+        entries.add(save(deviceId, 45000, 500));
+        entries.add(save(deviceId, 55000, 600.0));
+
+        List<TsKvEntry> list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
+                60000, 20000, 3, Aggregation.NONE))).get();
+        assertEquals(3, list.size());
+        assertEquals(55000, list.get(0).getTs());
+        assertEquals(java.util.Optional.of(600.0), list.get(0).getDoubleValue());
+
+        assertEquals(45000, list.get(1).getTs());
+        assertEquals(java.util.Optional.of(500L), list.get(1).getLongValue());
+
+        assertEquals(35000, list.get(2).getTs());
+        assertEquals(java.util.Optional.of(400.0), list.get(2).getDoubleValue());
+
+        list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
+                60000, 20000, 3, Aggregation.AVG))).get();
+        assertEquals(3, list.size());
+        assertEquals(10000, list.get(0).getTs());
+        assertEquals(java.util.Optional.of(150.0), list.get(0).getDoubleValue());
+
+        assertEquals(30000, list.get(1).getTs());
+        assertEquals(java.util.Optional.of(350.0), list.get(1).getDoubleValue());
+
+        assertEquals(50000, list.get(2).getTs());
+        assertEquals(java.util.Optional.of(550.0), list.get(2).getDoubleValue());
+
+        list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
+                60000, 20000, 3, Aggregation.SUM))).get();
+
+        assertEquals(3, list.size());
+        assertEquals(10000, list.get(0).getTs());
+        assertEquals(java.util.Optional.of(300.0), list.get(0).getDoubleValue());
+
+        assertEquals(30000, list.get(1).getTs());
+        assertEquals(java.util.Optional.of(700.0), list.get(1).getDoubleValue());
+
+        assertEquals(50000, list.get(2).getTs());
+        assertEquals(java.util.Optional.of(1100.0), list.get(2).getDoubleValue());
+
+        list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
+                60000, 20000, 3, Aggregation.MIN))).get();
+
+        assertEquals(3, list.size());
+        assertEquals(10000, list.get(0).getTs());
+        assertEquals(java.util.Optional.of(100.0), list.get(0).getDoubleValue());
+
+        assertEquals(30000, list.get(1).getTs());
+        assertEquals(java.util.Optional.of(300.0), list.get(1).getDoubleValue());
+
+        assertEquals(50000, list.get(2).getTs());
+        assertEquals(java.util.Optional.of(500.0), list.get(2).getDoubleValue());
+
+        list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
+                60000, 20000, 3, Aggregation.MAX))).get();
+
+        assertEquals(3, list.size());
+        assertEquals(10000, list.get(0).getTs());
+        assertEquals(java.util.Optional.of(200.0), list.get(0).getDoubleValue());
+
+        assertEquals(30000, list.get(1).getTs());
+        assertEquals(java.util.Optional.of(400.0), list.get(1).getDoubleValue());
+
+        assertEquals(50000, list.get(2).getTs());
+        assertEquals(java.util.Optional.of(600.0), list.get(2).getDoubleValue());
+
+        list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
+                60000, 20000, 3, Aggregation.COUNT))).get();
+
+        assertEquals(3, list.size());
+        assertEquals(10000, list.get(0).getTs());
+        assertEquals(java.util.Optional.of(2L), list.get(0).getLongValue());
+
+        assertEquals(30000, list.get(1).getTs());
+        assertEquals(java.util.Optional.of(2L), list.get(1).getLongValue());
+
+        assertEquals(50000, list.get(2).getTs());
+        assertEquals(java.util.Optional.of(2L), list.get(2).getLongValue());
+    }
+
     private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception {
         TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value));
         tsService.save(tenantId, deviceId, entry).get();
         return entry;
     }
 
+    private TsKvEntry save(DeviceId deviceId, long ts, double value) throws Exception {
+        TsKvEntry entry = new BasicTsKvEntry(ts, new DoubleDataEntry(LONG_KEY, value));
+        tsService.save(tenantId, deviceId, entry).get();
+        return entry;
+    }
+
+
     private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException {
         tsService.save(tenantId, deviceId, toTsEntry(ts, stringKvEntry)).get();
         tsService.save(tenantId, deviceId, toTsEntry(ts, longKvEntry)).get();

docker/README.md 42(+21 -21)

diff --git a/docker/README.md b/docker/README.md
index c43a136..d4655f8 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -1,4 +1,4 @@
-# Docker configuration for ThingsBoard Microservices 
+# Docker configuration for ThingsBoard Microservices
 
 This folder containing scripts and Docker Compose configurations to run ThingsBoard in Microservices mode.
 
@@ -9,19 +9,19 @@ Before starting please make sure [Docker CE](https://docs.docker.com/install/) a
 
 ## Installation
 
-Before performing initial installation you can configure the type of database to be used with ThinsBoard.
+Before performing initial installation you can configure the type of database to be used with ThingsBoard.
 In order to set database type change the value of `DATABASE` variable in `.env` file to one of the following:
 
 - `postgres` - use PostgreSQL database;
 - `cassandra` - use Cassandra database;
- 
-**NOTE**: According to the database type corresponding docker service will be deployed (see `docker-compose.postgres.yml`, `docker-compose.cassandra.yml` for details).  
+
+**NOTE**: According to the database type corresponding docker service will be deployed (see `docker-compose.postgres.yml`, `docker-compose.cassandra.yml` for details).
 
 Execute the following command to run installation:
 
-` 
+`
 $ ./docker-install-tb.sh --loadDemo
-` 
+`
 
 Where:
 
@@ -31,56 +31,56 @@ Where:
 
 Execute the following command to start services:
 
-` 
+`
 $ ./docker-start-services.sh
-` 
+`
 
 After a while when all services will be successfully started you can open `http://{your-host-ip}` in you browser (for ex. `http://localhost`).
 You should see ThingsBoard login page.
 
 Use the following default credentials:
 
-- **Systen Administrator**: sysadmin@thingsboard.org / sysadmin
+- **System Administrator**: sysadmin@thingsboard.org / sysadmin
 
 If you installed DataBase with demo data (using `--loadDemo` flag) you can also use the following credentials:
 
 - **Tenant Administrator**: tenant@thingsboard.org / tenant
 - **Customer User**: customer@thingsboard.org / customer
 
-In case of any issues you can examine service logs for errors. 
+In case of any issues you can examine service logs for errors.
 For example to see ThingsBoard node logs execute the following command:
 
-` 
+`
 $ docker-compose logs -f tb1
-` 
+`
 
 Or use `docker-compose ps` to see the state of all the containers.
 Use `docker-compose logs --f` to inspect the logs of all running services.
-See [docker-compose logs](https://docs.docker.com/compose/reference/logs/) command reference for details.  
+See [docker-compose logs](https://docs.docker.com/compose/reference/logs/) command reference for details.
 
 Execute the following command to stop services:
 
-` 
+`
 $ ./docker-stop-services.sh
-` 
+`
 
 Execute the following command to stop and completely remove deployed docker containers:
 
-` 
+`
 $ ./docker-remove-services.sh
-` 
+`
 
 Execute the following command to update particular or all services (pull newer docker image and rebuild container):
 
-` 
+`
 $ ./docker-update-service.sh [SERVICE...]
-` 
+`
 
 Where:
 
-- `[SERVICE...]` - list of services to update (defined in docker-compose configurations). If not specified all services will be updated. 
+- `[SERVICE...]` - list of services to update (defined in docker-compose configurations). If not specified all services will be updated.
 
-## Upgrading 
+## Upgrading
 
 In case when database upgrade is needed, execute the following commands:
 
diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java
index 4c3ac83..bbabc2e 100644
--- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java
+++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java
@@ -32,7 +32,8 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 import org.apache.http.ssl.SSLContextBuilder;
 import org.apache.http.ssl.SSLContexts;
-import org.junit.*;
+import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.rules.TestRule;
 import org.junit.rules.TestWatcher;
 import org.junit.runner.Description;
@@ -43,7 +44,10 @@ import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 
-import javax.net.ssl.*;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
 import java.net.URI;
 import java.security.cert.X509Certificate;
 import java.util.List;
@@ -54,6 +58,7 @@ import java.util.Random;
 public abstract class AbstractContainerTest {
     protected static final String HTTPS_URL = "https://localhost";
     protected static final String WSS_URL = "wss://localhost";
+    protected static String TB_TOKEN;
     protected static RestClient restClient;
     protected ObjectMapper mapper = new ObjectMapper();
 
diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java
index a6e89de..bb3380f 100644
--- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java
+++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.msa.connectivity;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.collect.Sets;
 import org.junit.Assert;
 import org.junit.Test;
@@ -25,6 +26,17 @@ import org.thingsboard.server.msa.AbstractContainerTest;
 import org.thingsboard.server.msa.WsClient;
 import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.thingsboard.server.common.data.DataConstants.DEVICE;
+import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
+
 public class HttpClientTest extends AbstractContainerTest {
 
     @Test
@@ -52,6 +64,58 @@ public class HttpClientTest extends AbstractContainerTest {
         Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
         Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
 
-        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
+        restClient.deleteDevice(device.getId());
+    }
+
+    @Test
+    public void getAttributes() throws Exception {
+        restClient.login("tenant@thingsboard.org", "tenant");
+        TB_TOKEN = restClient.getToken();
+
+        Device device = createDevice("test");
+        String accessToken = restClient.getCredentials(device.getId()).getCredentialsId();
+        assertNotNull(accessToken);
+
+        ResponseEntity deviceSharedAttributes = restClient.getRestTemplate()
+                .postForEntity(HTTPS_URL + "/api/plugins/telemetry/" + DEVICE + "/" + device.getId().toString() + "/attributes/" + SHARED_SCOPE, mapper.readTree(createPayload().toString()),
+                        ResponseEntity.class,
+                        accessToken);
+
+        Assert.assertTrue(deviceSharedAttributes.getStatusCode().is2xxSuccessful());
+
+        ResponseEntity deviceClientsAttributes = restClient.getRestTemplate()
+                .postForEntity(HTTPS_URL + "/api/v1/" + accessToken + "/attributes/", mapper.readTree(createPayload().toString()),
+                        ResponseEntity.class,
+                        accessToken);
+
+        Assert.assertTrue(deviceClientsAttributes.getStatusCode().is2xxSuccessful());
+
+        TimeUnit.SECONDS.sleep(3);
+
+        Optional<JsonNode> allOptional = restClient.getAttributes(accessToken, null, null);
+        assertTrue(allOptional.isPresent());
+
+
+        JsonNode all = allOptional.get();
+        assertEquals(2, all.size());
+        assertEquals(mapper.readTree(createPayload().toString()), all.get("shared"));
+        assertEquals(mapper.readTree(createPayload().toString()), all.get("client"));
+
+        Optional<JsonNode> sharedOptional = restClient.getAttributes(accessToken, null, "stringKey");
+        assertTrue(sharedOptional.isPresent());
+
+        JsonNode shared = sharedOptional.get();
+        assertEquals(shared.get("shared").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
+        assertFalse(shared.has("client"));
+
+        Optional<JsonNode> clientOptional = restClient.getAttributes(accessToken, "longKey,stringKey", null);
+        assertTrue(clientOptional.isPresent());
+
+        JsonNode client = clientOptional.get();
+        assertFalse(client.has("shared"));
+        assertEquals(mapper.readTree(createPayload().get("longKey").toString()), client.get("client").get("longKey"));
+        assertEquals(client.get("client").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
+
+        restClient.deleteDevice(device.getId());
     }
 }
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
index c927c42..d2a1ad1 100644
--- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
@@ -222,6 +222,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage> 
 
     private void handlePuback(MqttPubAckMessage message) {
         MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(message.variableHeader().messageId());
+        if (pendingPublish == null) {
+            return;
+        }
         pendingPublish.getFuture().setSuccess(null);
         pendingPublish.onPubackReceived();
         this.client.getPendingPublishes().remove(message.variableHeader().messageId());
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java
index 9f86b8e..000925e 100644
--- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java
@@ -15,8 +15,6 @@
  */
 package org.thingsboard.mqtt;
 
-import io.netty.channel.ChannelId;
-
 /**
  * Created by Valerii Sosliuk on 12/30/2017.
  */

pom.xml 18(+12 -6)

diff --git a/pom.xml b/pom.xml
index b3182f7..a4de43b 100755
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,7 @@
         <surfire.version>2.19.1</surfire.version>
         <jar-plugin.version>3.0.2</jar-plugin.version>
         <springfox-swagger.version>2.6.1</springfox-swagger.version>
+        <springfox-swagger-ui-rfc6570.version>1.0.0</springfox-swagger-ui-rfc6570.version>
         <bouncycastle.version>1.56</bouncycastle.version>
         <winsw.version>2.0.1</winsw.version>
         <hsqldb.version>2.4.0</hsqldb.version>
@@ -83,6 +84,7 @@
         <delight-nashorn-sandbox.version>0.1.14</delight-nashorn-sandbox.version>
         <kafka.version>2.0.0</kafka.version>
         <bucket4j.version>4.1.1</bucket4j.version>
+        <fst.version>2.57</fst.version>
     </properties>
 
     <modules>
@@ -756,12 +758,6 @@
                 <version>${hsqldb.version}</version>
             </dependency>
             <dependency>
-                <groupId>ru.yandex.qatools.embed</groupId>
-                <artifactId>postgresql-embedded</artifactId>
-                <version>2.2</version>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
                 <groupId>org.springframework.data</groupId>
                 <artifactId>spring-data-redis</artifactId>
                 <version>${spring-data-redis.version}</version>
@@ -794,6 +790,16 @@
                 <artifactId>bucket4j-core</artifactId>
                 <version>${bucket4j.version}</version>
             </dependency>
+            <dependency>
+                <groupId>de.ruedigermoeller</groupId>
+                <artifactId>fst</artifactId>
+                <version>${fst.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.springfox.ui</groupId>
+                <artifactId>springfox-swagger-ui-rfc6570</artifactId>
+                <version>${springfox-swagger-ui-rfc6570.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
index c3608fc..ffbd5eb 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
@@ -39,7 +39,9 @@ import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.customer.CustomerService;
@@ -47,6 +49,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.entityview.EntityViewService;
 
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
@@ -58,10 +61,8 @@ import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
 public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationActionNodeConfiguration> implements TbNode {
 
     protected C config;
-    protected EntityId fromId;
-    protected EntityId toId;
 
-    private LoadingCache<Entitykey, EntityContainer> entityIdCache;
+    private LoadingCache<EntityKey, EntityContainer> entityIdCache;
 
     @Override
     public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
@@ -84,7 +85,7 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
     public void destroy() {
     }
 
-    private ListenableFuture<Boolean> processEntityRelationAction(TbContext ctx, TbMsg msg) {
+    protected ListenableFuture<Boolean> processEntityRelationAction(TbContext ctx, TbMsg msg) {
         return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer));
     }
 
@@ -96,40 +97,58 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
 
     protected ListenableFuture<EntityContainer> getEntity(TbContext ctx, TbMsg msg) {
         String entityName = TbNodeUtils.processPattern(this.config.getEntityNamePattern(), msg.getMetaData());
-        String type = null;
+        String type;
         if (this.config.getEntityTypePattern() != null) {
             type = TbNodeUtils.processPattern(this.config.getEntityTypePattern(), msg.getMetaData());
+        } else {
+            type = null;
         }
         EntityType entityType = EntityType.valueOf(this.config.getEntityType());
-        Entitykey key = new Entitykey(entityName, type, entityType);
+        EntityKey key = new EntityKey(entityName, type, entityType);
         return ctx.getDbCallbackExecutor().executeAsync(() -> {
             EntityContainer entityContainer = entityIdCache.get(key);
             if (entityContainer.getEntityId() == null) {
-                throw new RuntimeException("No entity found with type '" + key.getEntityType() + " ' and name '" + key.getEntityName() + "'.");
+                throw new RuntimeException("No entity found with type '" + key.getEntityType() + "' and name '" + key.getEntityName() + "'.");
             }
             return entityContainer;
         });
     }
 
-    protected void processSearchDirection(TbMsg msg, EntityContainer entityContainer) {
+    protected SearchDirectionIds processSingleSearchDirection(TbMsg msg, EntityContainer entityContainer) {
+        SearchDirectionIds searchDirectionIds = new SearchDirectionIds();
         if (EntitySearchDirection.FROM.name().equals(config.getDirection())) {
-            fromId = EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString());
-            toId = msg.getOriginator();
+            searchDirectionIds.setFromId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
+            searchDirectionIds.setToId(msg.getOriginator());
         } else {
-            toId = EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString());
-            fromId = msg.getOriginator();
+            searchDirectionIds.setToId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
+            searchDirectionIds.setFromId(msg.getOriginator());
+        }
+        return searchDirectionIds;
+    }
+
+    protected ListenableFuture<List<EntityRelation>> processListSearchDirection(TbContext ctx, TbMsg msg) {
+        if (EntitySearchDirection.FROM.name().equals(config.getDirection())) {
+            return ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON);
+        } else {
+            return ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON);
         }
     }
 
     @Data
     @AllArgsConstructor
-    private static class Entitykey {
+    private static class EntityKey {
         private String entityName;
         private String type;
         private EntityType entityType;
     }
 
-    private static class EntityCacheLoader extends CacheLoader<Entitykey, EntityContainer> {
+    @Data
+    protected static class SearchDirectionIds {
+        private EntityId fromId;
+        private EntityId toId;
+    }
+
+    private static class EntityCacheLoader extends CacheLoader<EntityKey, EntityContainer> {
 
         private final TbContext ctx;
         private final boolean createIfNotExists;
@@ -140,11 +159,11 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
         }
 
         @Override
-        public EntityContainer load(Entitykey key) {
+        public EntityContainer load(EntityKey key) {
             return loadEntity(key);
         }
 
-        private EntityContainer loadEntity(Entitykey entitykey) {
+        private EntityContainer loadEntity(EntityKey entitykey) {
             EntityType type = entitykey.getEntityType();
             EntityContainer targetEntity = new EntityContainer();
             targetEntity.setEntityType(type);
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
index ac66629..6fb84a4 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
@@ -54,7 +54,8 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfig
 
     @Override
     protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
-        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
+        String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg.getMetaData());
+        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType);
         return Futures.transformAsync(latest, a -> {
             if (a != null && !a.getStatus().isCleared()) {
                 return clearAlarm(ctx, msg, a);
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
index a660c93..82a645c 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
@@ -16,6 +16,7 @@
 package org.thingsboard.rule.engine.action;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Function;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.msg.TbMsg;
 
+import java.io.IOException;
+
 @Slf4j
 @RuleNode(
         type = ComponentType.ACTION,
@@ -49,6 +52,8 @@ import org.thingsboard.server.common.msg.TbMsg;
 )
 public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
 
+    private static ObjectMapper mapper = new ObjectMapper();
+
     @Override
     protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
         return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
@@ -56,32 +61,59 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
 
     @Override
     protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
-        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
-        return Futures.transformAsync(latest, a -> {
-            if (a == null || a.getStatus().isCleared()) {
-                return createNewAlarm(ctx, msg);
+        String alarmType;
+        final Alarm msgAlarm;
+
+        if (!config.isUseMessageAlarmData()) {
+            alarmType = config.getAlarmType();
+            msgAlarm = null;
+        } else {
+            try {
+                msgAlarm = mapper.readValue(msg.getData(), Alarm.class);
+                msgAlarm.setTenantId(ctx.getTenantId());
+                alarmType = msgAlarm.getType();
+            } catch (IOException e) {
+                ctx.tellFailure(msg, e);
+                return null;
+            }
+        }
+
+        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType);
+        return Futures.transformAsync(latest, existingAlarm -> {
+            if (existingAlarm == null || existingAlarm.getStatus().isCleared()) {
+                return createNewAlarm(ctx, msg, msgAlarm);
             } else {
-                return updateAlarm(ctx, msg, a);
+                return updateAlarm(ctx, msg, existingAlarm, msgAlarm);
             }
         }, ctx.getDbCallbackExecutor());
 
     }
 
-    private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
-        ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
-                details -> buildAlarm(msg, details, ctx.getTenantId()));
+    private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) {
+        ListenableFuture<Alarm> asyncAlarm;
+        if (msgAlarm != null ) {
+            asyncAlarm = Futures.immediateCheckedFuture(msgAlarm);
+        } else {
+            asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
+                    details -> buildAlarm(msg, details, ctx.getTenantId()));
+        }
         ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
                 alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
         return Futures.transform(asyncCreated, alarm -> new AlarmResult(true, false, false, alarm));
     }
 
-    private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
-        ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, alarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
-            alarm.setSeverity(config.getSeverity());
-            alarm.setPropagate(config.isPropagate());
-            alarm.setDetails(details);
-            alarm.setEndTs(System.currentTimeMillis());
-            return ctx.getAlarmService().createOrUpdateAlarm(alarm);
+    private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) {
+        ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
+            if (msgAlarm != null) {
+                existingAlarm.setSeverity(msgAlarm.getSeverity());
+                existingAlarm.setPropagate(msgAlarm.isPropagate());
+            } else {
+                existingAlarm.setSeverity(config.getSeverity());
+                existingAlarm.setPropagate(config.isPropagate());
+            }
+            existingAlarm.setDetails(details);
+            existingAlarm.setEndTs(System.currentTimeMillis());
+            return ctx.getAlarmService().createOrUpdateAlarm(existingAlarm);
         }, ctx.getDbCallbackExecutor());
 
         return Futures.transform(asyncUpdated, a -> new AlarmResult(false, true, false, a));
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
index b424794..892ad27 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
@@ -24,6 +24,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
 
     private AlarmSeverity severity;
     private boolean propagate;
+    private boolean useMessageAlarmData;
 
     @Override
     public TbCreateAlarmNodeConfiguration defaultConfiguration() {
@@ -36,6 +37,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
         configuration.setAlarmType("General Alarm");
         configuration.setSeverity(AlarmSeverity.CRITICAL);
         configuration.setPropagate(false);
+        configuration.setUseMessageAlarmData(false);
         return configuration;
     }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
index a0b9709..bf73260 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
@@ -65,92 +65,91 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateR
     }
 
     private ListenableFuture<Boolean> createIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
-        processSearchDirection(msg, entityContainer);
-        return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON),
+        SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
+        return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON),
                 result -> {
                     if (!result) {
-                        return processCreateRelation(ctx, entityContainer);
+                        return processCreateRelation(ctx, entityContainer, sdId);
                     }
                     return Futures.immediateFuture(true);
                 });
     }
 
-    private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer) {
+    private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
         switch (entityContainer.getEntityType()) {
             case ASSET:
-                return processAsset(ctx, entityContainer);
+                return processAsset(ctx, entityContainer, sdId);
             case DEVICE:
-                return processDevice(ctx, entityContainer);
+                return processDevice(ctx, entityContainer, sdId);
             case CUSTOMER:
-                return processCustomer(ctx, entityContainer);
+                return processCustomer(ctx, entityContainer, sdId);
             case DASHBOARD:
-                return processDashboard(ctx, entityContainer);
+                return processDashboard(ctx, entityContainer, sdId);
             case ENTITY_VIEW:
-                return processView(ctx, entityContainer);
+                return processView(ctx, entityContainer, sdId);
             case TENANT:
-                return processTenant(ctx, entityContainer);
+                return processTenant(ctx, entityContainer, sdId);
         }
         return Futures.immediateFuture(true);
     }
 
-    private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer) {
+    private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
         return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(entityContainer.getEntityId().getId())), entityView -> {
             if (entityView != null) {
-                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
+                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON));
             } else {
                 return Futures.immediateFuture(true);
             }
         });
     }
 
-    private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer) {
+    private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
         return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())), device -> {
             if (device != null) {
-                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
+                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON));
             } else {
                 return Futures.immediateFuture(true);
             }
         });
     }
 
-    private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer) {
+    private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
         return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(entityContainer.getEntityId().getId())), asset -> {
             if (asset != null) {
-                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
+                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON));
             } else {
                 return Futures.immediateFuture(true);
             }
         });
     }
 
-    private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer) {
+    private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
         return Futures.transformAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), new CustomerId(entityContainer.getEntityId().getId())), customer -> {
             if (customer != null) {
-                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
+                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON));
             } else {
                 return Futures.immediateFuture(true);
             }
         });
     }
 
-    private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer) {
+    private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
         return Futures.transformAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), new DashboardId(entityContainer.getEntityId().getId())), dashboard -> {
             if (dashboard != null) {
-                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
+                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON));
             } else {
                 return Futures.immediateFuture(true);
             }
         });
     }
 
-    private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer) {
+    private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
         return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), new TenantId(entityContainer.getEntityId().getId())), tenant -> {
             if (tenant != null) {
-                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
+                return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON));
             } else {
                 return Futures.immediateFuture(true);
             }
         });
     }
-
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
index c339ffc..de9a1aa 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
@@ -25,18 +25,24 @@ import org.thingsboard.rule.engine.api.TbNodeException;
 import org.thingsboard.rule.engine.api.util.TbNodeUtils;
 import org.thingsboard.rule.engine.util.EntityContainer;
 import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.common.msg.TbMsg;
 
+import java.util.ArrayList;
+import java.util.List;
+
+
 @Slf4j
 @RuleNode(
         type = ComponentType.ACTION,
         name = "delete relation",
         configClazz = TbDeleteRelationNodeConfiguration.class,
-        nodeDescription = "Finds target Entity by entity name pattern and then delete a relation to Originator Entity by type and direction.",
-        nodeDetails = "If the relation successfully deleted -  Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
+        nodeDescription = "Finds target Entity by entity name pattern and then delete a relation to Originator Entity by type and direction" +
+                " if 'Delete single entity' is set to true, otherwise rule node will delete all relations to the originator of the message by type and direction.",
+        nodeDetails = "If the relation(s) successfully deleted -  Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
         uiResources = {"static/rulenode/rulenode-core-config.js"},
-        configDirective = "tbActionNodeDeleteRelationConfig",
+        configDirective ="tbActionNodeDeleteRelationConfig",
         icon = "remove_circle"
 )
 public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteRelationNodeConfiguration> {
@@ -52,23 +58,54 @@ public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteR
     }
 
     @Override
+    protected ListenableFuture<Boolean> processEntityRelationAction(TbContext ctx, TbMsg msg) {
+        if(config.isDeleteForSingleEntity()){
+            return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer));
+        } else {
+            return processList(ctx, msg);
+        }
+    }
+
+    @Override
     protected ListenableFuture<Boolean> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
-        return deleteIfExist(ctx, msg, entityContainer);
+        return processSingle(ctx, msg, entityContainer);
     }
 
-    private ListenableFuture<Boolean> deleteIfExist(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
-        processSearchDirection(msg, entityContainer);
-        return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON),
+    private ListenableFuture<Boolean> processList(TbContext ctx, TbMsg msg) {
+        return Futures.transformAsync(processListSearchDirection(ctx, msg), entityRelations -> {
+            if(entityRelations.isEmpty()){
+                return Futures.immediateFuture(true);
+            } else {
+                List<ListenableFuture<Boolean>> listenableFutureList = new ArrayList<>();
+                for (EntityRelation entityRelation: entityRelations) {
+                    listenableFutureList.add(ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), entityRelation));
+                }
+                return Futures.transformAsync(Futures.allAsList(listenableFutureList), booleans -> {
+                   for (Boolean bool :  booleans) {
+                       if (!bool) {
+                           return Futures.immediateFuture(false);
+                       }
+                   }
+                   return Futures.immediateFuture(true);
+                });
+            }
+        });
+    }
+
+    private ListenableFuture<Boolean> processSingle(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
+        SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
+        return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON),
+
                 result -> {
                     if (result) {
-                        return processDeleteRelation(ctx);
+                        return processSingleDeleteRelation(ctx, sdId);
                     }
                     return Futures.immediateFuture(true);
                 });
     }
 
-    private ListenableFuture<Boolean> processDeleteRelation(TbContext ctx) {
-        return ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON);
+    private ListenableFuture<Boolean> processSingleDeleteRelation(TbContext ctx, SearchDirectionIds sdId) {
+        return ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), config.getRelationType(), RelationTypeGroup.COMMON);
     }
 
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java
index f786404..b1919e2 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java
@@ -22,10 +22,12 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 @Data
 public class TbDeleteRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbDeleteRelationNodeConfiguration> {
 
+    private boolean deleteForSingleEntity;
 
     @Override
     public TbDeleteRelationNodeConfiguration defaultConfiguration() {
         TbDeleteRelationNodeConfiguration configuration = new TbDeleteRelationNodeConfiguration();
+        configuration.setDeleteForSingleEntity(true);
         configuration.setDirection(EntitySearchDirection.FROM.name());
         configuration.setRelationType("Contains");
         configuration.setEntityNamePattern("");
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
index f4c4642..c826a37 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
@@ -15,6 +15,8 @@
  */
 package org.thingsboard.rule.engine.filter;
 
+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;
@@ -25,13 +27,13 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.common.msg.TbMsg;
 
-import javax.management.relation.RelationType;
+import java.util.List;
 
-import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
 import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
 
 /**
@@ -43,8 +45,10 @@ import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
         name = "check relation",
         configClazz = TbCheckRelationNodeConfiguration.class,
         relationTypes = {"True", "False"},
-        nodeDescription = "Checks the relation from the selected entity to originator of the message by type and direction",
-        nodeDetails = "If relation exists - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.",
+        nodeDescription = "Checks the relation from the selected entity to the originator of the message by type and direction" +
+                " if 'Check for single entity' is set to true, otherwise rule node will check if exist" +
+                " any relation to the originator of the message by type and direction.",
+        nodeDetails = "If at least one relation exists - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.",
         uiResources = {"static/rulenode/rulenode-core-config.js"},
         configDirective = "tbFilterNodeCheckRelationConfig")
 public class TbCheckRelationNode implements TbNode {
@@ -58,6 +62,16 @@ public class TbCheckRelationNode implements TbNode {
 
     @Override
     public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+        ListenableFuture<Boolean> checkRelationFuture;
+        if (config.isCheckForSingleEntity()) {
+            checkRelationFuture = processSingle(ctx, msg);
+        } else {
+            checkRelationFuture = processList(ctx, msg);
+        }
+        withCallback(checkRelationFuture, filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+    }
+
+    private ListenableFuture<Boolean> processSingle(TbContext ctx, TbMsg msg) {
         EntityId from;
         EntityId to;
         if (EntitySearchDirection.FROM.name().equals(config.getDirection())) {
@@ -67,8 +81,25 @@ public class TbCheckRelationNode implements TbNode {
             to = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId());
             from = msg.getOriginator();
         }
-        withCallback(ctx.getRelationService().checkRelation(ctx.getTenantId(), from, to, config.getRelationType(), RelationTypeGroup.COMMON),
-                filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+        return ctx.getRelationService().checkRelation(ctx.getTenantId(), from, to, config.getRelationType(), RelationTypeGroup.COMMON);
+    }
+
+    private ListenableFuture<Boolean> processList(TbContext ctx, TbMsg msg) {
+        if (EntitySearchDirection.FROM.name().equals(config.getDirection())) {
+            return Futures.transformAsync(ctx.getRelationService()
+                    .findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON), this::isEmptyList);
+        } else {
+            return Futures.transformAsync(ctx.getRelationService()
+                    .findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON), this::isEmptyList);
+        }
+    }
+
+    private ListenableFuture<Boolean> isEmptyList(List<EntityRelation> entityRelations) {
+        if (entityRelations.isEmpty()) {
+            return Futures.immediateFuture(false);
+        } else {
+            return Futures.immediateFuture(true);
+        }
     }
 
     @Override
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java
index d0f00f5..b54b125 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java
@@ -18,10 +18,6 @@ package org.thingsboard.rule.engine.filter;
 import lombok.Data;
 import org.thingsboard.rule.engine.api.NodeConfiguration;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
-import org.thingsboard.server.common.msg.session.SessionMsgType;
-
-import java.util.Arrays;
-import java.util.List;
 
 /**
  * Created by ashvayka on 19.01.18.
@@ -33,12 +29,14 @@ public class TbCheckRelationNodeConfiguration implements NodeConfiguration<TbChe
     private String entityId;
     private String entityType;
     private String relationType;
+    private boolean checkForSingleEntity;
 
     @Override
     public TbCheckRelationNodeConfiguration defaultConfiguration() {
         TbCheckRelationNodeConfiguration configuration = new TbCheckRelationNodeConfiguration();
         configuration.setDirection(EntitySearchDirection.FROM.name());
         configuration.setRelationType("Contains");
+        configuration.setCheckForSingleEntity(true);
         return configuration;
     }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
index 66569b7..d636339 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
@@ -21,8 +21,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.util.concurrent.ListenableFuture;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.rule.engine.api.*;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
 import org.thingsboard.rule.engine.api.util.DonAsynchron;
 import org.thingsboard.rule.engine.api.util.TbNodeUtils;
 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
@@ -37,7 +44,9 @@ import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
-import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.*;
+import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL;
+import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST;
+import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE;
 import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
 
 /**
@@ -50,6 +59,7 @@ import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
         nodeDescription = "Add Message Originator Telemetry for selected time range into Message Metadata\n",
         nodeDetails = "The node allows you to select fetch mode <b>FIRST/LAST/ALL</b> to fetch telemetry of certain time range that are added into Message metadata without any prefix. " +
                 "If selected fetch mode <b>ALL</b> Telemetry will be added like array into Message Metadata where <b>key</b> is Timestamp and <b>value</b> is value of Telemetry. " +
+                "<b>Note</b>: The maximum size of the fetched array is 1000 records. " +
                 "If selected fetch mode <b>FIRST</b> or <b>LAST</b> Telemetry will be added like string without Timestamp",
         uiResources = {"static/rulenode/rulenode-core-config.js"},
         configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase")
@@ -57,8 +67,6 @@ public class TbGetTelemetryNode implements TbNode {
 
     private TbGetTelemetryNodeConfiguration config;
     private List<String> tsKeyNames;
-    private long startTsOffset;
-    private long endTsOffset;
     private int limit;
     private ObjectMapper mapper;
     private String fetchMode;
@@ -67,8 +75,6 @@ public class TbGetTelemetryNode implements TbNode {
     public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
         this.config = TbNodeUtils.convert(configuration, TbGetTelemetryNodeConfiguration.class);
         tsKeyNames = config.getLatestTsKeyNames();
-        startTsOffset = TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval());
-        endTsOffset = TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval());
         limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? MAX_FETCH_SIZE : 1;
         fetchMode = config.getFetchMode();
         mapper = new ObjectMapper();
@@ -82,8 +88,10 @@ public class TbGetTelemetryNode implements TbNode {
             ctx.tellFailure(msg, new IllegalStateException("Telemetry is not selected!"));
         } else {
             try {
-                List<ReadTsKvQuery> queries = buildQueries();
-                ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), queries);
+                if (config.isUseMetadataIntervalPatterns()) {
+                    checkMetadataKeyPatterns(msg);
+                }
+                ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(msg));
                 DonAsynchron.withCallback(list, data -> {
                     process(data, msg);
                     TbMsg newMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData());
@@ -95,10 +103,12 @@ public class TbGetTelemetryNode implements TbNode {
         }
     }
 
-    private List<ReadTsKvQuery> buildQueries() {
-        long ts = System.currentTimeMillis();
-        long startTs = ts - startTsOffset;
-        long endTs = ts - endTsOffset;
+    @Override
+    public void destroy() {
+
+    }
+
+    private List<ReadTsKvQuery> buildQueries(TbMsg msg) {
         String orderBy;
         if (fetchMode.equals(FETCH_MODE_FIRST) || fetchMode.equals(FETCH_MODE_ALL)) {
             orderBy = "ASC";
@@ -106,7 +116,7 @@ public class TbGetTelemetryNode implements TbNode {
             orderBy = "DESC";
         }
         return tsKeyNames.stream()
-                .map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE, orderBy))
+                .map(key -> new BaseReadTsKvQuery(key, getInterval(msg).getStartTs(), getInterval(msg).getEndTs(), 1, limit, NONE, orderBy))
                 .collect(Collectors.toList());
     }
 
@@ -162,8 +172,79 @@ public class TbGetTelemetryNode implements TbNode {
         return obj;
     }
 
-    @Override
-    public void destroy() {
+    private Interval getInterval(TbMsg msg) {
+        Interval interval = new Interval();
+        if (config.isUseMetadataIntervalPatterns()) {
+            if (isParsable(msg, config.getStartIntervalPattern())) {
+                interval.setStartTs(Long.parseLong(TbNodeUtils.processPattern(config.getStartIntervalPattern(), msg.getMetaData())));
+            }
+            if (isParsable(msg, config.getEndIntervalPattern())) {
+                interval.setEndTs(Long.parseLong(TbNodeUtils.processPattern(config.getEndIntervalPattern(), msg.getMetaData())));
+            }
+        } else {
+            long ts = System.currentTimeMillis();
+            interval.setStartTs(ts - TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval()));
+            interval.setEndTs(ts - TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval()));
+        }
+        return interval;
+    }
+
+    private boolean isParsable(TbMsg msg, String pattern) {
+        return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg.getMetaData()));
+    }
 
+    private void checkMetadataKeyPatterns(TbMsg msg) {
+        isUndefined(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern());
+        isInvalid(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern());
     }
+
+    private void isUndefined(TbMsg msg, String startIntervalPattern, String endIntervalPattern) {
+        if (getMetadataValue(msg, startIntervalPattern) == null && getMetadataValue(msg, endIntervalPattern) == null) {
+            throw new IllegalArgumentException("Message metadata values: '" +
+                    replaceRegex(startIntervalPattern) + "' and '" +
+                    replaceRegex(endIntervalPattern) + "' are undefined");
+        } else {
+            if (getMetadataValue(msg, startIntervalPattern) == null) {
+                throw new IllegalArgumentException("Message metadata value: '" +
+                        replaceRegex(startIntervalPattern) + "' is undefined");
+            }
+            if (getMetadataValue(msg, endIntervalPattern) == null) {
+                throw new IllegalArgumentException("Message metadata value: '" +
+                        replaceRegex(endIntervalPattern) + "' is undefined");
+            }
+        }
+    }
+
+    private void isInvalid(TbMsg msg, String startIntervalPattern, String endIntervalPattern) {
+        if (getInterval(msg).getStartTs() == null && getInterval(msg).getEndTs() == null) {
+            throw new IllegalArgumentException("Message metadata values: '" +
+                    replaceRegex(startIntervalPattern) + "' and '" +
+                    replaceRegex(endIntervalPattern) + "' have invalid format");
+        } else {
+            if (getInterval(msg).getStartTs() == null) {
+                throw new IllegalArgumentException("Message metadata value: '" +
+                        replaceRegex(startIntervalPattern) + "' has invalid format");
+            }
+            if (getInterval(msg).getEndTs() == null) {
+                throw new IllegalArgumentException("Message metadata value: '" +
+                        replaceRegex(endIntervalPattern) + "' has invalid format");
+            }
+        }
+    }
+
+    private String getMetadataValue(TbMsg msg, String pattern) {
+        return msg.getMetaData().getValue(replaceRegex(pattern));
+    }
+
+    private String replaceRegex(String pattern) {
+        return pattern.replaceAll("[${}]", "");
+    }
+
+    @Data
+    @NoArgsConstructor
+    private static class Interval {
+        private Long startTs;
+        private Long endTs;
+    }
+
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java
index 37ce0af..31bacf9 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java
@@ -35,6 +35,12 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
 
     private int startInterval;
     private int endInterval;
+
+    private String startIntervalPattern;
+    private String endIntervalPattern;
+
+    private boolean useMetadataIntervalPatterns;
+
     private String startIntervalTimeUnit;
     private String endIntervalTimeUnit;
     private String fetchMode; //FIRST, LAST, LATEST
@@ -52,6 +58,9 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
         configuration.setStartInterval(2);
         configuration.setEndIntervalTimeUnit(TimeUnit.MINUTES.name());
         configuration.setEndInterval(1);
+        configuration.setUseMetadataIntervalPatterns(false);
+        configuration.setStartIntervalPattern("");
+        configuration.setEndIntervalPattern("");
         return configuration;
     }
 }
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
index 3101b87..b07f8d0 100644
--- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -1,5 +1,5 @@
-!function(e){function t(i){if(n[i])return n[i].exports;var r=n[i]={exports:{},id:i,loaded:!1};return e[i].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),i=e[t[0]];return function(e,t,r){i.apply(this,[e,t,r].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(89)},function(e,t){},1,1,1,1,function(e,t){e.exports=" <section ng-form name=assignCustomerConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.customer-name-pattern</label> <input ng-required=true name=customerNamePattern ng-model=configuration.customerNamePattern> <div ng-messages=assignCustomerConfigForm.customerNamePattern.$error> <div ng-message=required translate>tb.rulenode.customer-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.customer-name-pattern-hint</div> </md-input-container> <md-checkbox aria-label=\"{{ 'tb.rulenode.create-group-if-not-exists' | translate }}\" ng-model=configuration.createCustomerIfNotExists>{{ 'tb.rulenode.create-customer-if-not-exists' | translate }} </md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.customer-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=customerCacheExpiration ng-model=configuration.customerCacheExpiration> <div ng-messages=assignCustomerConfigForm.customerCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.customer-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.customer-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.customer-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=attributesConfigForm layout=column> <md-input-container class=md-block> <label translate>attribute.attributes-scope</label> <md-select ng-model=configuration.scope ng-disabled=$root.loading> <md-option ng-repeat="scope in types.attributesScope" ng-value=scope.value> {{scope.name | translate}} </md-option> </md-select> </md-input-container> </section> '},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <md-input-container class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <section layout=column layout-gt-sm=row> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section ng-form name=createRelationConfigForm layout=column style=min-width:650px> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select> <md-input-container class=md-block> <tb-entity-type-select style=min-width:100px the-form=createRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> </md-input-container> <md-input-container class=md-block flex ng-if=configuration.entityType style=margin-top:38px> <label translate>tb.rulenode.entity-name-pattern</label> <input ng-required=true name=entityNamePattern ng-model=configuration.entityNamePattern> <div ng-messages=createRelationConfigForm.entityNamePattern.$error> <div ng-message=required translate>tb.rulenode.entity-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.entity-name-pattern-hint</div> </md-input-container> <md-input-container class=md-block flex ng-if=\"configuration.entityType  == 'DEVICE' || configuration.entityType  == 'ASSET'\" style=margin-top:38px> <label translate>tb.rulenode.entity-type-pattern</label> <input ng-required=true name=entityTypePattern ng-model=configuration.entityTypePattern> <div ng-messages=createRelationConfigForm.entityTypePattern.$error> <div ng-message=required translate>tb.rulenode.entity-type-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.entity-type-pattern-hint</div> </md-input-container> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> <md-checkbox flex ng-if=\"configuration.entityType  == 'CUSTOMER' || configuration.entityType  == 'ASSET' || configuration.entityType  == 'DEVICE'\" aria-label=\"{{ 'tb.rulenode.create-entity-if-not-exists' | translate }}\" ng-model=configuration.createEntityIfNotExists>{{ 'tb.rulenode.create-entity-if-not-exists' | translate }} </md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.entity-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=entityCacheExpiration ng-model=configuration.entityCacheExpiration> <div ng-messages=createRelationConfigForm.entityCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.entity-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.entity-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.entity-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=deleteRelationConfigForm layout=column> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select> <md-input-container class=md-block> <tb-entity-type-select style=min-width:100px the-form=deleteRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> </md-input-container> <md-input-container class=md-block flex ng-if=configuration.entityType style=margin-top:38px> <label translate>tb.rulenode.entity-name-pattern</label> <input ng-required=true name=entityNamePattern ng-model=configuration.entityNamePattern> <div ng-messages=deleteRelationConfigForm.entityNamePattern.$error> <div ng-message=required translate>tb.rulenode.entity-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.entity-name-pattern-hint</div> </md-input-container> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> <md-input-container class=md-block> <label translate>tb.rulenode.entity-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=entityCacheExpiration ng-model=configuration.entityCacheExpiration> <div ng-messages=deleteRelationConfigForm.entityCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.entity-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.entity-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.entity-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section ng-form name=kafkaConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=kafkaConfigForm.topicPattern.$error> <div ng-message=required translate>tb.rulenode.topic-pattern-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bootstrap-servers</label> <input ng-required=true name=bootstrapServers ng-model=configuration.bootstrapServers> <div ng-messages=kafkaConfigForm.bootstrapServers.$error> <div ng-message=required translate>tb.rulenode.bootstrap-servers-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.retries</label> <input type=number step=1 name=retries ng-model=configuration.retries min=0> <div ng-messages=kafkaConfigForm.retries.$error> <div ng-message=min translate>tb.rulenode.min-retries-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.batch-size-bytes</label> <input type=number step=1 name=batchSize ng-model=configuration.batchSize min=0> <div ng-messages=kafkaConfigForm.batchSize.$error> <div ng-message=min translate>tb.rulenode.min-batch-size-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.linger-ms</label> <input type=number step=1 name=linger ng-model=configuration.linger min=0> <div ng-messages=kafkaConfigForm.linger.$error> <div ng-message=min translate>tb.rulenode.min-linger-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.buffer-memory-bytes</label> <input type=number step=1 name=bufferMemory ng-model=configuration.bufferMemory min=0> <div ng-messages=kafkaConfigForm.bufferMemory.$error> <div ng-message=min translate>tb.rulenode.min-buffer-memory-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.acks</label> <md-select ng-model=configuration.acks ng-disabled=$root.loading> <md-option ng-repeat="ackValue in ackValues" ng-value=ackValue> {{ ackValue }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.key-serializer</label> <input ng-required=true name=keySerializer ng-model=configuration.keySerializer> <div ng-messages=kafkaConfigForm.keySerializer.$error> <div ng-message=required translate>tb.rulenode.key-serializer-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.value-serializer</label> <input ng-required=true name=valueSerializer ng-model=configuration.valueSerializer> <div ng-messages=kafkaConfigForm.valueSerializer.$error> <div ng-message=required translate>tb.rulenode.value-serializer-required</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.other-properties</label> <tb-kv-map-config ng-model=configuration.otherProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-mqtt-config ng-form name=mqttConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=mqttConfigForm.topicPattern.$error> <div translate ng-message=required>tb.rulenode.topic-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.mqtt-topic-pattern-hint</div> </md-input-container> <div flex layout=column layout-gt-sm=row> <md-input-container flex=60 class=md-block> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=mqttConfigForm.host.$error> <div translate ng-message=required>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.port> <div ng-messages=mqttConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.port-required</div> <div translate ng-message=min>tb.rulenode.port-range</div> <div translate ng-message=max>tb.rulenode.port-range</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.connect-timeout</label> <input type=number step=1 min=1 max=200 ng-required=true name=connectTimeoutSec ng-model=configuration.connectTimeoutSec> <div ng-messages=mqttConfigForm.connectTimeoutSec.$error> <div translate ng-message=required>tb.rulenode.connect-timeout-required</div> <div translate ng-message=min>tb.rulenode.connect-timeout-range</div> <div translate ng-message=max>tb.rulenode.connect-timeout-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.client-id</label> <input name=clientId ng-model=configuration.clientId> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.clean-session\' | translate }}" ng-model=configuration.cleanSession> {{ \'tb.rulenode.clean-session\' | translate }} </md-checkbox> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-ssl\' | translate }}" ng-model=configuration.ssl> {{ \'tb.rulenode.enable-ssl\' | translate }} </md-checkbox> <md-expansion-panel-group class=tb-credentials-panel-group ng-class="{\'disabled\': $root.loading || readonly}" md-component-id=credentialsPanelGroup> <md-expansion-panel md-component-id=credentialsPanel> <md-expansion-panel-collapsed> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-collapsed> <md-expansion-panel-expanded> <md-expansion-panel-header ng-click="$mdExpansionPanel(\'credentialsPanel\').collapse()"> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-header> <md-expansion-panel-content> <div layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.credentials-type</label> <md-select ng-required=true name=credentialsType ng-model=configuration.credentials.type ng-disabled="$root.loading || readonly" ng-change=credentialsTypeChanged()> <md-option ng-repeat="(credentialsType, credentialsValue) in ruleNodeTypes.mqttCredentialTypes" ng-value=credentialsValue.value> {{credentialsValue.name | translate}} </md-option> </md-select> <div ng-messages=mqttConfigForm.credentialsType.$error> <div translate ng-message=required>tb.rulenode.credentials-type-required</div> </div> </md-input-container> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes.basic.value"> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input ng-required=true name=mqttUsername ng-model=configuration.credentials.username> <div ng-messages=mqttConfigForm.mqttUsername.$error> <div translate ng-message=required>tb.rulenode.username-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input type=password ng-required=true name=mqttPassword ng-model=configuration.credentials.password> <div ng-messages=mqttConfigForm.mqttPassword.$error> <div translate ng-message=required>tb.rulenode.password-required</div> </div> </md-input-container> </section> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes[\'cert.PEM\'].value" class=dropdown-section> <div class=tb-container ng-class="configuration.credentials.caCertFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.ca-cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'caCert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'caCert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=caCertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=caCertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.caCertFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.caCertFileName>{{configuration.credentials.caCertFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.certFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'Cert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'Cert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=CertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=CertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.certFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.certFileName>{{configuration.credentials.certFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.privateKeyFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.private-key</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'privateKey\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'privateKey\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=privateKeySelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=privateKeySelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.privateKeyFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.privateKeyFileName>{{configuration.credentials.privateKeyFileName}}</div> </div> <md-input-container class=md-block> <label translate>tb.rulenode.private-key-password</label> <input type=password name=privateKeyPassword ng-model=configuration.credentials.password> </md-input-container> </section> </div> </md-expansion-panel-content> </md-expansion-panel-expanded> </md-expansion-panel> </md-expansion-panel-group> </section>'},function(e,t){e.exports=" <section ng-form name=msgCountConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.interval-seconds</label> <input ng-required=true type=number step=1 name=interval ng-model=configuration.interval min=1> <div ng-messages=msgCountConfigForm.interval.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.interval-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-interval-seconds-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.output-timeseries-key-prefix</label> <input ng-required=true name=telemetryPrefix ng-model=configuration.telemetryPrefix> <div ng-messages=msgCountConfigForm.telemetryPrefix.$error> <div translate ng-message=required>tb.rulenode.output-timeseries-key-prefix-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=msgDelayConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=0> <div ng-messages=msgDelayConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-0-seconds-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-pending-messages</label> <input ng-required=true type=number step=1 name=maxPendingMsgs ng-model=configuration.maxPendingMsgs min=1 max=100000> <div ng-messages=msgDelayConfigForm.maxPendingMsgs.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.max-pending-messages-required</div> <div ng-message=min translate>tb.rulenode.max-pending-messages-range</div> <div ng-message=max translate>tb.rulenode.max-pending-messages-range</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=rabbitMqConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.exchange-name-pattern</label> <input name=exchangeNamePattern ng-model=configuration.exchangeNamePattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.routing-key-pattern</label> <input name=routingKeyPattern ng-model=configuration.routingKeyPattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.message-properties</label> <md-select ng-model=configuration.messageProperties ng-disabled="$root.loading || readonly"> <md-option ng-repeat="property in messageProperties" ng-value=property> {{ property }} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=rabbitMqConfigForm.host.$error> <div ng-message=required translate>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.port</label> <input ng-required=true type=number step=1 name=port ng-model=configuration.port min=0 max=65535> <div ng-messages=rabbitMqConfigForm.port.$error> <div ng-message=required translate>tb.rulenode.port-required</div> <div ng-message=min translate>tb.rulenode.port-range</div> <div ng-message=max translate>tb.rulenode.port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.virtual-host</label> <input name=virtualHost ng-model=configuration.virtualHost> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=virtualHost ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=virtualHost type=password ng-model=configuration.password> </md-input-container> <md-input-container class=md-block> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.automatic-recovery\' | translate }}" ng-model=ruleNode.automaticRecoveryEnabled>{{ \'tb.rulenode.automatic-recovery\' | translate }} </md-checkbox> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.connection-timeout-ms</label> <input type=number step=1 name=connectionTimeout ng-model=configuration.connectionTimeout min=0> <div ng-messages=rabbitMqConfigForm.connectionTimeout.$error> <div ng-message=min translate>tb.rulenode.min-connection-timeout-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.handshake-timeout-ms</label> <input type=number step=1 name=handshakeTimeout ng-model=configuration.handshakeTimeout min=0> <div ng-messages=rabbitMqConfigForm.handshakeTimeout.$error> <div ng-message=min translate>tb.rulenode.min-handshake-timeout-ms-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.client-properties</label> <tb-kv-map-config ng-model=configuration.clientProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=' <section ng-form name=restApiCallConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.endpoint-url-pattern</label> <input ng-required=true name=endpointUrlPattern ng-model=configuration.restEndpointUrlPattern> <div ng-messages=restApiCallConfigForm.endpointUrlPattern.$error> <div ng-message=required translate>tb.rulenode.endpoint-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.endpoint-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.request-method</label> <md-select ng-model=configuration.requestMethod ng-disabled=$root.loading> <md-option ng-repeat="type in ruleNodeTypes.httpRequestType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <label translate class=tb-title>tb.rulenode.headers</label> <div class=tb-hint translate>tb.rulenode.headers-hint</div> <tb-kv-map-config ng-model=configuration.headers ng-required=false key-text="\'tb.rulenode.header\'" key-required-text="\'tb.rulenode.header-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section ng-form name=rpcReplyConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.request-id-metadata-attribute</label> <input name=requestIdMetaDataAttribute ng-model=configuration.requestIdMetaDataAttribute> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=rpcRequestConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-sec</label> <input ng-required=true type=number step=1 name=timeoutInSeconds ng-model=configuration.timeoutInSeconds min=0> <div ng-messages=rpcRequestConfigForm.timeoutInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.timeout-required</div> <div ng-message=min translate>tb.rulenode.min-timeout-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sendEmailConfigForm layout=column> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}" ng-model=configuration.useSystemSmtpSettings> {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} </md-checkbox> <section layout=column ng-if=!configuration.useSystemSmtpSettings> <md-input-container class=md-block> <label translate>tb.rulenode.smtp-protocol</label> <md-select ng-disabled="$root.loading || readonly" ng-model=configuration.smtpProtocol> <md-option ng-repeat="smtpProtocol in smtpProtocols" value={{smtpProtocol}}> {{smtpProtocol.toUpperCase()}} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.smtp-host</label> <input ng-required=true name=smtpHost ng-model=configuration.smtpHost> <div ng-messages=sendEmailConfigForm.smtpHost.$error> <div translate ng-message=required>tb.rulenode.smtp-host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.smtp-port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.smtpPort> <div ng-messages=sendEmailConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.smtp-port-required</div> <div translate ng-message=min>tb.rulenode.smtp-port-range</div> <div translate ng-message=max>tb.rulenode.smtp-port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-msec</label> <input type=number step=1 min=0 ng-required=true name=timeout ng-model=configuration.timeout> <div ng-messages=sendEmailConfigForm.timeout.$error> <div translate ng-message=required>tb.rulenode.timeout-required</div> <div translate ng-message=min>tb.rulenode.min-timeout-msec-message</div> </div> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-tls\' | translate }}" ng-model=configuration.enableTls>{{ \'tb.rulenode.enable-tls\' | translate }}</md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=username placeholder="{{ \'tb.rulenode.enter-username\' | translate }}" ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=password placeholder="{{ \'tb.rulenode.enter-password\' | translate }}" type=password ng-model=configuration.password> </md-input-container> </section> </section> ';
-},function(e,t){e.exports=" <section ng-form name=snsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-arn-pattern</label> <input ng-required=true name=topicArnPattern ng-model=configuration.topicArnPattern> <div ng-messages=snsConfigForm.topicArnPattern.$error> <div ng-message=required translate>tb.rulenode.topic-arn-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.topic-arn-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sqsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.queue-type</label> <md-select ng-model=configuration.queueType ng-disabled="$root.loading || readonly"> <md-option ng-repeat="type in ruleNodeTypes.sqsQueueType" ng-value=type.value> {{ type.name | translate }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.queue-url-pattern</label> <input ng-required=true name=queueUrlPattern ng-model=configuration.queueUrlPattern> <div ng-messages=sqsConfigForm.queueUrlPattern.$error> <div ng-message=required translate>tb.rulenode.queue-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.queue-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block ng-if="configuration.queueType == ruleNodeTypes.sqsQueueType.STANDARD.value"> <label translate>tb.rulenode.delay-seconds</label> <input type=number step=1 name=delaySeconds ng-model=configuration.delaySeconds min=0 max=900> <div ng-messages=sqsConfigForm.delaySeconds.$error> <div ng-message=min translate>tb.rulenode.min-delay-seconds-message</div> <div ng-message=max translate>tb.rulenode.max-delay-seconds-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.message-attributes</label> <div class=tb-hint translate>tb.rulenode.message-attributes-hint</div> <tb-kv-map-config ng-model=configuration.messageAttributes ng-required=false key-text="\'tb.rulenode.name\'" key-required-text="\'tb.rulenode.name-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> '},function(e,t){e.exports=" <section ng-form name=timeseriesConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=timeseriesConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=unAssignCustomerConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.customer-name-pattern</label> <input ng-required=true name=customerNamePattern ng-model=configuration.customerNamePattern> <div ng-messages=unAssignCustomerConfigForm.customerNamePattern.$error> <div ng-message=required translate>tb.rulenode.customer-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.customer-name-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.customer-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=customerCacheExpiration ng-model=configuration.customerCacheExpiration> <div ng-messages=unAssignCustomerConfigForm.customerCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.customer-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.customer-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.customer-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=' <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat="direction in types.entitySearchDirection" ng-value=direction> {{ (\'relation.search-direction.\' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder="{{ \'tb.rulenode.unlimited-level\' | translate }}" ng-model=query.maxLevel aria-label="{{ \'tb.rulenode.max-relation-level\' | translate }}"> </md-input-container> </div> <div class=md-caption style=color:rgba(0,0,0,.57) translate>relation.relation-type</div> <tb-relation-type-autocomplete flex hide-label ng-model=query.relationType tb-required=false> </tb-relation-type-autocomplete> <div class="md-caption tb-required" style=color:rgba(0,0,0,.57) translate>device.device-types</div> <tb-entity-subtype-list tb-required=true entity-type=types.entityType.device ng-model=query.deviceTypes> </tb-entity-subtype-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.device-relations-query</label> <tb-device-relations-query-config style=padding-bottom:15px ng-model=configuration.deviceRelationsQuery> </tb-device-relations-query-config> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section class=tb-telemetry-from-database-config ng-form name=getTelemetryConfigForm layout=column> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <md-input-container style=margin-bottom:38px;margin-top:58px> <label translate class="tb-title no-padding">tb.rulenode.fetch-mode</label> <md-select required ng-model=configuration.fetchMode> <md-option ng-repeat="type in ruleNodeTypes.fetchModeType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value"> <label translate class="tb-title no-padding">tb.rulenode.start-interval</label> <input required type=number step=1 min=1 max=2147483647 name=startInterval ng-model=configuration.startInterval> <div ng-messages=getTelemetryConfigForm.startInterval.$error> <div translate ng-message=required>tb.rulenode.start-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit"> <label translate class="tb-title no-padding">tb.rulenode.start-interval-time-unit</label> <md-select required name=startIntervalTimeUnit aria-label="{{ \'tb.rulenode.start-interval-time-unit\' | translate }}" ng-model=configuration.startIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value"> <label translate class="tb-title no-padding">tb.rulenode.end-interval</label> <input required type=number step=1 min=1 max=2147483647 name=endInterval ng-model=configuration.endInterval> <div ng-messages=getTelemetryConfigForm.endInterval.$error> <div translate ng-message=required>tb.rulenode.end-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit"> <label translate class="tb-title no-padding">tb.rulenode.end-interval-time-unit</label> <md-select required name=endIntervalTimeUnit aria-label="{{ \'tb.rulenode.end-interval-time-unit\' | translate }}" ng-model=configuration.endIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.fields-mapping</label> <tb-kv-map-config ng-model=configuration.fieldsMapping ng-required=true required-text="\'tb.rulenode.fields-mapping-required\'" key-text="\'tb.rulenode.source-field\'" key-required-text="\'tb.rulenode.source-field-required\'" val-text="\'tb.rulenode.target-attribute\'" val-required-text="\'tb.rulenode.target-attribute-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},28,function(e,t){e.exports=" <section ng-form name=checkRelationConfigForm> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select> <tb-entity-type-select style=min-width:100px the-form=checkRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> <tb-entity-autocomplete flex ng-if=configuration.entityType the-form=checkRelationConfigForm tb-required=true entity-type=configuration.entityType ng-model=configuration.entityId> </tb-entity-autocomplete> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" class=required>tb.rulenode.originator-types-filter</label> <tb-entity-type-list flex ng-model=configuration.originatorTypes allowed-entity-types=allowedEntityTypes ignore-authority-filter=true tb-required=true> </tb-entity-type-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.from-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.subject-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.body-template-hint</div> </md-input-container> </section> "},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(6),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(7),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,i){var r=function(r,a,l,s){var u=o.default;a.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(8),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,i){var r=function(r,a,l,s){var u=o.default;a.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(9),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(10),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(11),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,i){var r=function(r,a,l,s){var u=o.default;a.html(u),r.types=n,r.originator=null,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue,r.configuration.originatorId&&r.configuration.originatorType?r.originator={id:r.configuration.originatorId,entityType:r.configuration.originatorType}:r.originator=null,r.$watch("originator",function(e,t){angular.equals(e,t)||(r.originator?(s.$viewValue.originatorId=r.originator.id,s.$viewValue.originatorType=r.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},r.testScript=function(e){var n=angular.copy(r.configuration.jsScript);i.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,s.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var a=n(12),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(65),a=i(r),o=n(46),l=i(o),s=n(51),u=i(s),d=n(48),c=i(d),m=n(47),g=i(m),p=n(54),f=i(p),b=n(60),v=i(b),y=n(61),q=i(y),h=n(59),$=i(h),x=n(53),T=i(x),k=n(63),C=i(k),w=n(64),M=i(w),_=n(58),S=i(_),N=n(55),E=i(N),V=n(62),P=i(V),F=n(57),j=i(F),A=n(56),O=i(A),I=n(45),R=i(I),K=n(66),D=i(K),U=n(50),L=i(U),z=n(49),B=i(z);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",a.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",$.default).directive("tbActionNodeKafkaConfig",T.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",E.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",j.default).directive("tbActionNodeMsgCountConfig",O.default).directive("tbActionNodeAssignToCustomerConfig",R.default).directive("tbActionNodeUnAssignToCustomerConfig",D.default).directive("tbActionNodeDeleteRelationConfig",L.default).directive("tbActionNodeCreateRelationConfig",B.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",
-scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(13),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var i=function(i,r,a,l){var s=o.default;r.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var r=angular.copy(i.configuration.jsScript);n.testNodeScript(e,r,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(14),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var i=function(i,r,a,l){var s=o.default;r.html(s),i.$mdExpansionPanel=t,i.ruleNodeTypes=n,i.credentialsTypeChanged=function(){var e=i.configuration.credentials.type;i.configuration.credentials={},i.configuration.credentials.type=e,i.updateValidity()},i.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){i.$apply(function(){if(n.target.result){l.$setDirty();var r=n.target.result;r&&r.length>0&&("caCert"==t&&(i.configuration.credentials.caCertFileName=e.name,i.configuration.credentials.caCert=r),"privateKey"==t&&(i.configuration.credentials.privateKeyFileName=e.name,i.configuration.credentials.privateKey=r),"Cert"==t&&(i.configuration.credentials.certFileName=e.name,i.configuration.credentials.cert=r)),i.updateValidity()}})},n.readAsText(e.file)},i.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(i.configuration.credentials.caCertFileName=null,i.configuration.credentials.caCert=null),"privateKey"==e&&(i.configuration.credentials.privateKeyFileName=null,i.configuration.credentials.privateKey=null),"Cert"==e&&(i.configuration.credentials.certFileName=null,i.configuration.credentials.cert=null),i.updateValidity()},i.updateValidity=function(){var e=!0,t=i.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:i}}r.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(2);var a=n(15),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(16),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(17),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(18),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(19),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(20),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(21),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(22),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(23),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(24),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(25),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(26),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||a.$setViewValue(n.query)}),a.$render=function(){n.query=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(27),o=i(a)},function(e,t){"use strict";function n(e){var t=function(t,n,i,r){n.html("<div></div>"),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(28),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(29),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var i=function(i,r,a,l){var s=o.default;r.html(s);var u=186;i.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,u],i.ruleNodeTypes=n,i.aggPeriodTimeUnits={},i.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,i.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,i.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,i.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,i.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{},link:i}}r.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(30),o=i(a);n(3)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(73),a=i(r),o=n(74),l=i(o),s=n(70),u=i(s),d=n(75),c=i(d),m=n(69),g=i(m),p=n(76),f=i(p),b=n(71),v=i(b);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",a.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",u.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(31),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(32),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(33),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(34),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(35),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(81),a=i(r),o=n(79),l=i(o),s=n(82),u=i(s),d=n(77),c=i(d),m=n(80),g=i(m);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",a.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var i=function(i,r,a,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<i.messageTypes.length;t++)e.push(i.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(i.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;r.html(d),i.selectedMessageType=null,i.messageTypeSearchText=null,i.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}i.transformMessageTypeChip=function(e){var n,i=t("filter")(c,{name:e},!0);return n=i&&i.length?angular.copy(i[0]):{name:e,value:e}},i.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},i.createMessageType=function(e,t){var n=angular.element(t,r)[0].firstElementChild,i=angular.element(n),a=i.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),i.scope().$mdChipsCtrl.appendChip(a.trim()),i.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){i.messageTypesWatch&&(i.messageTypesWatch(),i.messageTypesWatch=null);var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var r=0;r<e.messageTypes.length;r++){var a=e.messageTypes[r];n.messageType[a]?t.push(angular.copy(n.messageType[a])):t.push({name:a,value:a})}i.messageTypes=t,i.messageTypesWatch=i.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:i}}r.$inject=["$compile","$filter","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(4);var a=n(36),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.allowedEntityTypes=[t.entityType.device,t.entityType.asset,t.entityType.tenant,t.entityType.customer,t.entityType.user,t.entityType.dashboard,t.entityType.rulechain,t.entityType.rulenode],n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(37),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var i=function(i,r,a,l){var s=o.default;r.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var r=angular.copy(i.configuration.jsScript);n.testNodeScript(e,r,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(38),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var i=function(i,r,a,l){var s=o.default;r.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var r=angular.copy(i.configuration.jsScript);n.testNodeScript(e,r,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(39),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){function a(e){e>-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=a,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(40),o=i(a);n(5)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||a.$setViewValue(n.query)}),a.$render=function(){n.query=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(41),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,i,r,a){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||a.$setViewValue(n.configuration)}),a.$render=function(){n.configuration=a.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(42),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(85),a=i(r),o=n(87),l=i(o),s=n(88),u=i(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",a.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var i=function(i,r,a,l){var s=o.default;r.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var r=angular.copy(i.configuration.jsScript);n.testNodeScript(e,r,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(43),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,i,r){var a=o.default;n.html(a),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(44),o=i(a)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(92),a=i(r),o=n(78),l=i(o),s=n(72),u=i(s),d=n(86),c=i(d),m=n(52),g=i(m),p=n(68),f=i(p),b=n(84),v=i(b),y=n(67),q=i(y),h=n(83),$=i(h),x=n(91),T=i(x);t.default=angular.module("thingsboard.ruleChain.config",[a.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",$.default).config(T.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647'.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use <code>${metaKeyName}</code> to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use <code>${metaKeyName}</code> to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use <code>${metaKeyName}</code> to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use <code>${metaKeyName}</code> to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","request-method":"Request method",headers:"Headers","headers-hint":"Use <code>${metaKeyName}</code> in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern",
-"queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","message-attributes-hint":"Use <code>${metaKeyName}</code> in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required."},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};e.translations("en_US",t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e){(0,o.default)(e)}r.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var a=n(90),o=i(a)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},fetchModeType:["FIRST","LAST","ALL"],httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},timeUnit:{MILLISECONDS:{value:"MILLISECONDS",name:"tb.rulenode.time-unit-milliseconds"},SECONDS:{value:"SECONDS",name:"tb.rulenode.time-unit-seconds"},MINUTES:{value:"MINUTES",name:"tb.rulenode.time-unit-minutes"},HOURS:{value:"HOURS",name:"tb.rulenode.time-unit-hours"},DAYS:{value:"DAYS",name:"tb.rulenode.time-unit-days"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}]));
+!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={exports:{},id:i,loaded:!1};return e[i].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),i=e[t[0]];return function(e,t,a){i.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(89)},function(e,t){},1,1,1,1,function(e,t){e.exports=" <section ng-form name=assignCustomerConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.customer-name-pattern</label> <input ng-required=true name=customerNamePattern ng-model=configuration.customerNamePattern> <div ng-messages=assignCustomerConfigForm.customerNamePattern.$error> <div ng-message=required translate>tb.rulenode.customer-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.customer-name-pattern-hint</div> </md-input-container> <md-checkbox aria-label=\"{{ 'tb.rulenode.create-group-if-not-exists' | translate }}\" ng-model=configuration.createCustomerIfNotExists>{{ 'tb.rulenode.create-customer-if-not-exists' | translate }} </md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.customer-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=customerCacheExpiration ng-model=configuration.customerCacheExpiration> <div ng-messages=assignCustomerConfigForm.customerCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.customer-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.customer-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.customer-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=attributesConfigForm layout=column> <md-input-container class=md-block> <label translate>attribute.attributes-scope</label> <md-select ng-model=configuration.scope ng-disabled=$root.loading> <md-option ng-repeat="scope in types.attributesScope" ng-value=scope.value> {{scope.name | translate}} </md-option> </md-select> </md-input-container> </section> '},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <md-input-container class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> <div class=tb-hint translate>tb.rulenode.entity-type-pattern-hint</div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <md-checkbox aria-label=\"{{ 'tb.rulenode.use-metadata-interval-patterns' | translate }}\" ng-model=configuration.useMessageAlarmData>{{ 'tb.rulenode.use-message-alarm-data' | translate }} </md-checkbox> <section layout=column layout-gt-sm=row ng-if=!configuration.useMessageAlarmData> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <section layout=column layout-gt-sm=row ng-if=!configuration.useMessageAlarmData> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> </section> "},function(e,t){e.exports=" <section ng-form name=createRelationConfigForm layout=column style=min-width:650px> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select> <md-input-container class=md-block> <tb-entity-type-select style=min-width:100px the-form=createRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> </md-input-container> <md-input-container class=md-block flex ng-if=configuration.entityType style=margin-top:38px> <label translate>tb.rulenode.entity-name-pattern</label> <input ng-required=true name=entityNamePattern ng-model=configuration.entityNamePattern> <div ng-messages=createRelationConfigForm.entityNamePattern.$error> <div ng-message=required translate>tb.rulenode.entity-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.entity-name-pattern-hint</div> </md-input-container> <md-input-container class=md-block flex ng-if=\"configuration.entityType  == 'DEVICE' || configuration.entityType  == 'ASSET'\" style=margin-top:38px> <label translate>tb.rulenode.entity-type-pattern</label> <input ng-required=true name=entityTypePattern ng-model=configuration.entityTypePattern> <div ng-messages=createRelationConfigForm.entityTypePattern.$error> <div ng-message=required translate>tb.rulenode.entity-type-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.entity-type-pattern-hint</div> </md-input-container> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> <md-checkbox flex ng-if=\"configuration.entityType  == 'CUSTOMER' || configuration.entityType  == 'ASSET' || configuration.entityType  == 'DEVICE'\" aria-label=\"{{ 'tb.rulenode.create-entity-if-not-exists' | translate }}\" ng-model=configuration.createEntityIfNotExists>{{ 'tb.rulenode.create-entity-if-not-exists' | translate }} </md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.entity-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=entityCacheExpiration ng-model=configuration.entityCacheExpiration> <div ng-messages=createRelationConfigForm.entityCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.entity-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.entity-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.entity-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=deleteRelationConfigForm layout=column> <md-checkbox aria-label=\"{{ 'tb.rulenode.delete-relation-to-specific-entity' | translate }}\" ng-model=configuration.deleteForSingleEntity> {{ 'tb.rulenode.delete-relation-to-specific-entity' | translate }} </md-checkbox> <div class=tb-hint translate>tb.rulenode.delete-relation-hint</div> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select ng-if=configuration.deleteForSingleEntity> <md-input-container class=md-block> <tb-entity-type-select style=min-width:100px the-form=deleteRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> </md-input-container> <md-input-container class=md-block flex ng-if=configuration.entityType style=margin-top:38px> <label translate>tb.rulenode.entity-name-pattern</label> <input ng-required=true name=entityNamePattern ng-model=configuration.entityNamePattern> <div ng-messages=deleteRelationConfigForm.entityNamePattern.$error> <div ng-message=required translate>tb.rulenode.entity-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.entity-name-pattern-hint</div> </md-input-container> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> <md-input-container class=md-block> <label translate>tb.rulenode.entity-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=entityCacheExpiration ng-model=configuration.entityCacheExpiration> <div ng-messages=deleteRelationConfigForm.entityCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.entity-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.entity-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.entity-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section ng-form name=kafkaConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=kafkaConfigForm.topicPattern.$error> <div ng-message=required translate>tb.rulenode.topic-pattern-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bootstrap-servers</label> <input ng-required=true name=bootstrapServers ng-model=configuration.bootstrapServers> <div ng-messages=kafkaConfigForm.bootstrapServers.$error> <div ng-message=required translate>tb.rulenode.bootstrap-servers-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.retries</label> <input type=number step=1 name=retries ng-model=configuration.retries min=0> <div ng-messages=kafkaConfigForm.retries.$error> <div ng-message=min translate>tb.rulenode.min-retries-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.batch-size-bytes</label> <input type=number step=1 name=batchSize ng-model=configuration.batchSize min=0> <div ng-messages=kafkaConfigForm.batchSize.$error> <div ng-message=min translate>tb.rulenode.min-batch-size-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.linger-ms</label> <input type=number step=1 name=linger ng-model=configuration.linger min=0> <div ng-messages=kafkaConfigForm.linger.$error> <div ng-message=min translate>tb.rulenode.min-linger-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.buffer-memory-bytes</label> <input type=number step=1 name=bufferMemory ng-model=configuration.bufferMemory min=0> <div ng-messages=kafkaConfigForm.bufferMemory.$error> <div ng-message=min translate>tb.rulenode.min-buffer-memory-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.acks</label> <md-select ng-model=configuration.acks ng-disabled=$root.loading> <md-option ng-repeat="ackValue in ackValues" ng-value=ackValue> {{ ackValue }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.key-serializer</label> <input ng-required=true name=keySerializer ng-model=configuration.keySerializer> <div ng-messages=kafkaConfigForm.keySerializer.$error> <div ng-message=required translate>tb.rulenode.key-serializer-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.value-serializer</label> <input ng-required=true name=valueSerializer ng-model=configuration.valueSerializer> <div ng-messages=kafkaConfigForm.valueSerializer.$error> <div ng-message=required translate>tb.rulenode.value-serializer-required</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.other-properties</label> <tb-kv-map-config ng-model=configuration.otherProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-mqtt-config ng-form name=mqttConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=mqttConfigForm.topicPattern.$error> <div translate ng-message=required>tb.rulenode.topic-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.mqtt-topic-pattern-hint</div> </md-input-container> <div flex layout=column layout-gt-sm=row> <md-input-container flex=60 class=md-block> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=mqttConfigForm.host.$error> <div translate ng-message=required>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.port> <div ng-messages=mqttConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.port-required</div> <div translate ng-message=min>tb.rulenode.port-range</div> <div translate ng-message=max>tb.rulenode.port-range</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.connect-timeout</label> <input type=number step=1 min=1 max=200 ng-required=true name=connectTimeoutSec ng-model=configuration.connectTimeoutSec> <div ng-messages=mqttConfigForm.connectTimeoutSec.$error> <div translate ng-message=required>tb.rulenode.connect-timeout-required</div> <div translate ng-message=min>tb.rulenode.connect-timeout-range</div> <div translate ng-message=max>tb.rulenode.connect-timeout-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.client-id</label> <input name=clientId ng-model=configuration.clientId> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.clean-session\' | translate }}" ng-model=configuration.cleanSession> {{ \'tb.rulenode.clean-session\' | translate }} </md-checkbox> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-ssl\' | translate }}" ng-model=configuration.ssl> {{ \'tb.rulenode.enable-ssl\' | translate }} </md-checkbox> <md-expansion-panel-group class=tb-credentials-panel-group ng-class="{\'disabled\': $root.loading || readonly}" md-component-id=credentialsPanelGroup> <md-expansion-panel md-component-id=credentialsPanel> <md-expansion-panel-collapsed> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-collapsed> <md-expansion-panel-expanded> <md-expansion-panel-header ng-click="$mdExpansionPanel(\'credentialsPanel\').collapse()"> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-header> <md-expansion-panel-content> <div layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.credentials-type</label> <md-select ng-required=true name=credentialsType ng-model=configuration.credentials.type ng-disabled="$root.loading || readonly" ng-change=credentialsTypeChanged()> <md-option ng-repeat="(credentialsType, credentialsValue) in ruleNodeTypes.mqttCredentialTypes" ng-value=credentialsValue.value> {{credentialsValue.name | translate}} </md-option> </md-select> <div ng-messages=mqttConfigForm.credentialsType.$error> <div translate ng-message=required>tb.rulenode.credentials-type-required</div> </div> </md-input-container> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes.basic.value"> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input ng-required=true name=mqttUsername ng-model=configuration.credentials.username> <div ng-messages=mqttConfigForm.mqttUsername.$error> <div translate ng-message=required>tb.rulenode.username-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input type=password ng-required=true name=mqttPassword ng-model=configuration.credentials.password> <div ng-messages=mqttConfigForm.mqttPassword.$error> <div translate ng-message=required>tb.rulenode.password-required</div> </div> </md-input-container> </section> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes[\'cert.PEM\'].value" class=dropdown-section> <div class=tb-container ng-class="configuration.credentials.caCertFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.ca-cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'caCert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'caCert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=caCertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=caCertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.caCertFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.caCertFileName>{{configuration.credentials.caCertFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.certFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'Cert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'Cert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=CertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=CertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.certFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.certFileName>{{configuration.credentials.certFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.privateKeyFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.private-key</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'privateKey\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'privateKey\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=privateKeySelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=privateKeySelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.privateKeyFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.privateKeyFileName>{{configuration.credentials.privateKeyFileName}}</div> </div> <md-input-container class=md-block> <label translate>tb.rulenode.private-key-password</label> <input type=password name=privateKeyPassword ng-model=configuration.credentials.password> </md-input-container> </section> </div> </md-expansion-panel-content> </md-expansion-panel-expanded> </md-expansion-panel> </md-expansion-panel-group> </section>'},function(e,t){e.exports=" <section ng-form name=msgCountConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.interval-seconds</label> <input ng-required=true type=number step=1 name=interval ng-model=configuration.interval min=1> <div ng-messages=msgCountConfigForm.interval.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.interval-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-interval-seconds-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.output-timeseries-key-prefix</label> <input ng-required=true name=telemetryPrefix ng-model=configuration.telemetryPrefix> <div ng-messages=msgCountConfigForm.telemetryPrefix.$error> <div translate ng-message=required>tb.rulenode.output-timeseries-key-prefix-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=msgDelayConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=0> <div ng-messages=msgDelayConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-0-seconds-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-pending-messages</label> <input ng-required=true type=number step=1 name=maxPendingMsgs ng-model=configuration.maxPendingMsgs min=1 max=100000> <div ng-messages=msgDelayConfigForm.maxPendingMsgs.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.max-pending-messages-required</div> <div ng-message=min translate>tb.rulenode.max-pending-messages-range</div> <div ng-message=max translate>tb.rulenode.max-pending-messages-range</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=rabbitMqConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.exchange-name-pattern</label> <input name=exchangeNamePattern ng-model=configuration.exchangeNamePattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.routing-key-pattern</label> <input name=routingKeyPattern ng-model=configuration.routingKeyPattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.message-properties</label> <md-select ng-model=configuration.messageProperties ng-disabled="$root.loading || readonly"> <md-option ng-repeat="property in messageProperties" ng-value=property> {{ property }} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=rabbitMqConfigForm.host.$error> <div ng-message=required translate>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.port</label> <input ng-required=true type=number step=1 name=port ng-model=configuration.port min=0 max=65535> <div ng-messages=rabbitMqConfigForm.port.$error> <div ng-message=required translate>tb.rulenode.port-required</div> <div ng-message=min translate>tb.rulenode.port-range</div> <div ng-message=max translate>tb.rulenode.port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.virtual-host</label> <input name=virtualHost ng-model=configuration.virtualHost> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=virtualHost ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=virtualHost type=password ng-model=configuration.password> </md-input-container> <md-input-container class=md-block> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.automatic-recovery\' | translate }}" ng-model=ruleNode.automaticRecoveryEnabled>{{ \'tb.rulenode.automatic-recovery\' | translate }} </md-checkbox> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.connection-timeout-ms</label> <input type=number step=1 name=connectionTimeout ng-model=configuration.connectionTimeout min=0> <div ng-messages=rabbitMqConfigForm.connectionTimeout.$error> <div ng-message=min translate>tb.rulenode.min-connection-timeout-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.handshake-timeout-ms</label> <input type=number step=1 name=handshakeTimeout ng-model=configuration.handshakeTimeout min=0> <div ng-messages=rabbitMqConfigForm.handshakeTimeout.$error> <div ng-message=min translate>tb.rulenode.min-handshake-timeout-ms-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.client-properties</label> <tb-kv-map-config ng-model=configuration.clientProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=' <section ng-form name=restApiCallConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.endpoint-url-pattern</label> <input ng-required=true name=endpointUrlPattern ng-model=configuration.restEndpointUrlPattern> <div ng-messages=restApiCallConfigForm.endpointUrlPattern.$error> <div ng-message=required translate>tb.rulenode.endpoint-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.endpoint-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.request-method</label> <md-select ng-model=configuration.requestMethod ng-disabled=$root.loading> <md-option ng-repeat="type in ruleNodeTypes.httpRequestType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <label translate class=tb-title>tb.rulenode.headers</label> <div class=tb-hint translate>tb.rulenode.headers-hint</div> <tb-kv-map-config ng-model=configuration.headers ng-required=false key-text="\'tb.rulenode.header\'" key-required-text="\'tb.rulenode.header-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section ng-form name=rpcReplyConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.request-id-metadata-attribute</label> <input name=requestIdMetaDataAttribute ng-model=configuration.requestIdMetaDataAttribute> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=rpcRequestConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-sec</label> <input ng-required=true type=number step=1 name=timeoutInSeconds ng-model=configuration.timeoutInSeconds min=0> <div ng-messages=rpcRequestConfigForm.timeoutInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.timeout-required</div> <div ng-message=min translate>tb.rulenode.min-timeout-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sendEmailConfigForm layout=column> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}" ng-model=configuration.useSystemSmtpSettings> {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} </md-checkbox> <section layout=column ng-if=!configuration.useSystemSmtpSettings> <md-input-container class=md-block> <label translate>tb.rulenode.smtp-protocol</label> <md-select ng-disabled="$root.loading || readonly" ng-model=configuration.smtpProtocol> <md-option ng-repeat="smtpProtocol in smtpProtocols" value={{smtpProtocol}}> {{smtpProtocol.toUpperCase()}} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.smtp-host</label> <input ng-required=true name=smtpHost ng-model=configuration.smtpHost> <div ng-messages=sendEmailConfigForm.smtpHost.$error> <div translate ng-message=required>tb.rulenode.smtp-host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.smtp-port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.smtpPort> <div ng-messages=sendEmailConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.smtp-port-required</div> <div translate ng-message=min>tb.rulenode.smtp-port-range</div> <div translate ng-message=max>tb.rulenode.smtp-port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-msec</label> <input type=number step=1 min=0 ng-required=true name=timeout ng-model=configuration.timeout> <div ng-messages=sendEmailConfigForm.timeout.$error> <div translate ng-message=required>tb.rulenode.timeout-required</div> <div translate ng-message=min>tb.rulenode.min-timeout-msec-message</div> </div> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-tls\' | translate }}" ng-model=configuration.enableTls>{{ \'tb.rulenode.enable-tls\' | translate }}</md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=username placeholder="{{ \'tb.rulenode.enter-username\' | translate }}" ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=password placeholder="{{ \'tb.rulenode.enter-password\' | translate }}" type=password ng-model=configuration.password> </md-input-container> </section> </section> ';
+},function(e,t){e.exports=" <section ng-form name=snsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-arn-pattern</label> <input ng-required=true name=topicArnPattern ng-model=configuration.topicArnPattern> <div ng-messages=snsConfigForm.topicArnPattern.$error> <div ng-message=required translate>tb.rulenode.topic-arn-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.topic-arn-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sqsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.queue-type</label> <md-select ng-model=configuration.queueType ng-disabled="$root.loading || readonly"> <md-option ng-repeat="type in ruleNodeTypes.sqsQueueType" ng-value=type.value> {{ type.name | translate }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.queue-url-pattern</label> <input ng-required=true name=queueUrlPattern ng-model=configuration.queueUrlPattern> <div ng-messages=sqsConfigForm.queueUrlPattern.$error> <div ng-message=required translate>tb.rulenode.queue-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.queue-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block ng-if="configuration.queueType == ruleNodeTypes.sqsQueueType.STANDARD.value"> <label translate>tb.rulenode.delay-seconds</label> <input type=number step=1 name=delaySeconds ng-model=configuration.delaySeconds min=0 max=900> <div ng-messages=sqsConfigForm.delaySeconds.$error> <div ng-message=min translate>tb.rulenode.min-delay-seconds-message</div> <div ng-message=max translate>tb.rulenode.max-delay-seconds-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.message-attributes</label> <div class=tb-hint translate>tb.rulenode.message-attributes-hint</div> <tb-kv-map-config ng-model=configuration.messageAttributes ng-required=false key-text="\'tb.rulenode.name\'" key-required-text="\'tb.rulenode.name-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> '},function(e,t){e.exports=" <section ng-form name=timeseriesConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=timeseriesConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=unAssignCustomerConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.customer-name-pattern</label> <input ng-required=true name=customerNamePattern ng-model=configuration.customerNamePattern> <div ng-messages=unAssignCustomerConfigForm.customerNamePattern.$error> <div ng-message=required translate>tb.rulenode.customer-name-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.customer-name-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.customer-cache-expiration</label> <input type=number step=1 min=0 ng-required=true name=customerCacheExpiration ng-model=configuration.customerCacheExpiration> <div ng-messages=unAssignCustomerConfigForm.customerCacheExpiration.$error> <div translate ng-message=required>tb.rulenode.customer-cache-expiration-required</div> <div translate ng-message=min>tb.rulenode.customer-cache-expiration-range</div> </div> <div class=tb-hint translate>tb.rulenode.customer-cache-expiration-hint</div> </md-input-container> </section> "},function(e,t){e.exports=' <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat="direction in types.entitySearchDirection" ng-value=direction> {{ (\'relation.search-direction.\' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder="{{ \'tb.rulenode.unlimited-level\' | translate }}" ng-model=query.maxLevel aria-label="{{ \'tb.rulenode.max-relation-level\' | translate }}"> </md-input-container> </div> <div class=md-caption style=color:rgba(0,0,0,.57) translate>relation.relation-type</div> <tb-relation-type-autocomplete flex hide-label ng-model=query.relationType tb-required=false> </tb-relation-type-autocomplete> <div class="md-caption tb-required" style=color:rgba(0,0,0,.57) translate>device.device-types</div> <tb-entity-subtype-list tb-required=true entity-type=types.entityType.device ng-model=query.deviceTypes> </tb-entity-subtype-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.device-relations-query</label> <tb-device-relations-query-config style=padding-bottom:15px ng-model=configuration.deviceRelationsQuery> </tb-device-relations-query-config> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section class=tb-telemetry-from-database-config ng-form name=getTelemetryConfigForm layout=column> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <md-input-container style=margin-bottom:38px;margin-top:58px> <label translate class="tb-title no-padding">tb.rulenode.fetch-mode</label> <md-select required ng-model=configuration.fetchMode> <md-option ng-repeat="type in ruleNodeTypes.fetchModeType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <md-checkbox aria-label="{{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}" ng-model=configuration.useMetadataIntervalPatterns>{{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }} </md-checkbox> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value" flex ng-if="configuration.useMetadataIntervalPatterns == false"> <label translate class="tb-title no-padding">tb.rulenode.start-interval</label> <input required type=number step=1 min=1 max=2147483647 name=startInterval ng-model=configuration.startInterval> <div ng-messages=getTelemetryConfigForm.startInterval.$error> <div translate ng-message=required>tb.rulenode.start-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit" flex ng-if="configuration.useMetadataIntervalPatterns == false "> <label translate class="tb-title no-padding">tb.rulenode.start-interval-time-unit</label> <md-select required name=startIntervalTimeUnit aria-label="{{ \'tb.rulenode.start-interval-time-unit\' | translate }}" ng-model=configuration.startIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value" flex ng-if="configuration.useMetadataIntervalPatterns == false"> <label translate class="tb-title no-padding">tb.rulenode.end-interval</label> <input required type=number step=1 min=1 max=2147483647 name=endInterval ng-model=configuration.endInterval> <div ng-messages=getTelemetryConfigForm.endInterval.$error> <div translate ng-message=required>tb.rulenode.end-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit" flex ng-if="configuration.useMetadataIntervalPatterns === false"> <label translate class="tb-title no-padding">tb.rulenode.end-interval-time-unit</label> <md-select required name=endIntervalTimeUnit aria-label="{{ \'tb.rulenode.end-interval-time-unit\' | translate }}" ng-model=configuration.endIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> <md-input-container class=md-block flex ng-if="configuration.useMetadataIntervalPatterns === true" style=margin-top:38px> <label translate>tb.rulenode.start-interval-pattern</label> <input ng-required=true name=startIntervalPattern ng-model=configuration.startIntervalPattern> <div ng-messages=getTelemetryConfigForm.startIntervalPattern.$error> <div ng-message=required translate>tb.rulenode.start-interval-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.start-interval-pattern-hint</div> </md-input-container> <md-input-container class=md-block flex ng-if="configuration.useMetadataIntervalPatterns === true"> <label translate>tb.rulenode.end-interval-pattern</label> <input ng-required=true name=endIntervalPattern ng-model=configuration.endIntervalPattern> <div ng-messages=getTelemetryConfigForm.endIntervalPattern.$error> <div ng-message=required translate>tb.rulenode.end-interval-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.end-interval-pattern-hint</div> </md-input-container> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.fields-mapping</label> <tb-kv-map-config ng-model=configuration.fieldsMapping ng-required=true required-text="\'tb.rulenode.fields-mapping-required\'" key-text="\'tb.rulenode.source-field\'" key-required-text="\'tb.rulenode.source-field-required\'" val-text="\'tb.rulenode.target-attribute\'" val-required-text="\'tb.rulenode.target-attribute-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},28,function(e,t){e.exports=" <section ng-form name=checkRelationConfigForm> <md-checkbox aria-label=\"{{ 'tb.rulenode.check-relation-to-specific-entity' | translate }}\" ng-model=configuration.checkForSingleEntity> {{ 'tb.rulenode.check-relation-to-specific-entity' | translate }} </md-checkbox> <div class=tb-hint translate>tb.rulenode.check-relation-hint</div> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select ng-if=configuration.checkForSingleEntity style=padding-top:20px> <tb-entity-type-select style=min-width:100px;padding-bottom:20px the-form=checkRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> <tb-entity-autocomplete flex ng-if=configuration.entityType the-form=checkRelationConfigForm tb-required=true entity-type=configuration.entityType ng-model=configuration.entityId> </tb-entity-autocomplete> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" class=required>tb.rulenode.originator-types-filter</label> <tb-entity-type-list flex ng-model=configuration.originatorTypes allowed-entity-types=allowedEntityTypes ignore-authority-filter=true tb-required=true> </tb-entity-type-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.from-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.subject-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.body-template-hint</div> </md-input-container> </section> "},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(6),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(7),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var u=o.default;r.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(8),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var u=o.default;r.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(9),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(10),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(11),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var u=o.default;r.html(u),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript);i.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var r=n(12),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e
+}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(65),r=i(a),o=n(46),l=i(o),s=n(51),u=i(s),d=n(48),c=i(d),m=n(47),g=i(m),p=n(54),f=i(p),b=n(60),v=i(b),y=n(61),q=i(y),h=n(59),$=i(h),x=n(53),k=i(x),T=n(63),C=i(T),w=n(64),M=i(w),S=n(58),_=i(S),N=n(55),E=i(N),P=n(62),V=i(P),F=n(57),j=i(F),A=n(56),I=i(A),O=n(45),D=i(O),K=n(66),R=i(K),U=n(50),L=i(U),z=n(49),B=i(z);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",r.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",$.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",_.default).directive("tbActionNodeMqttConfig",E.default).directive("tbActionNodeSendEmailConfig",V.default).directive("tbActionNodeMsgDelayConfig",j.default).directive("tbActionNodeMsgCountConfig",I.default).directive("tbActionNodeAssignToCustomerConfig",D.default).directive("tbActionNodeUnAssignToCustomerConfig",R.default).directive("tbActionNodeDeleteRelationConfig",L.default).directive("tbActionNodeCreateRelationConfig",B.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(13),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(14),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$mdExpansionPanel=t,i.ruleNodeTypes=n,i.credentialsTypeChanged=function(){var e=i.configuration.credentials.type;i.configuration.credentials={},i.configuration.credentials.type=e,i.updateValidity()},i.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){i.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(i.configuration.credentials.caCertFileName=e.name,i.configuration.credentials.caCert=a),"privateKey"==t&&(i.configuration.credentials.privateKeyFileName=e.name,i.configuration.credentials.privateKey=a),"Cert"==t&&(i.configuration.credentials.certFileName=e.name,i.configuration.credentials.cert=a)),i.updateValidity()}})},n.readAsText(e.file)},i.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(i.configuration.credentials.caCertFileName=null,i.configuration.credentials.caCert=null),"privateKey"==e&&(i.configuration.credentials.privateKeyFileName=null,i.configuration.credentials.privateKey=null),"Cert"==e&&(i.configuration.credentials.certFileName=null,i.configuration.credentials.cert=null),i.updateValidity()},i.updateValidity=function(){var e=!0,t=i.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:i}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var r=n(15),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(16),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(17),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(18),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(19),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(20),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(21),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(22),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(23),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(24),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(25),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(26),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(27),o=i(r)},function(e,t){"use strict";function n(e){var t=function(t,n,i,a){n.html("<div></div>"),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(28),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(29),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s);var u=186;i.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,u],i.ruleNodeTypes=n,i.aggPeriodTimeUnits={},i.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,i.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,i.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,i.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,i.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{},link:i}}a.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(30),o=i(r);n(3)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(73),r=i(a),o=n(74),l=i(o),s=n(70),u=i(s),d=n(75),c=i(d),m=n(69),g=i(m),p=n(76),f=i(p),b=n(71),v=i(b);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",r.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",u.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(31),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(32),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(33),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(34),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(35),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(81),r=i(a),o=n(79),l=i(o),s=n(82),u=i(s),d=n(77),c=i(d),m=n(80),g=i(m);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",r.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<i.messageTypes.length;t++)e.push(i.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(i.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;a.html(d),i.selectedMessageType=null,i.messageTypeSearchText=null,i.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}i.transformMessageTypeChip=function(e){var n,i=t("filter")(c,{name:e},!0);return n=i&&i.length?angular.copy(i[0]):{name:e,value:e}},i.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},i.createMessageType=function(e,t){var n=angular.element(t,a)[0].firstElementChild,i=angular.element(n),r=i.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),i.scope().$mdChipsCtrl.appendChip(r.trim()),i.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){i.messageTypesWatch&&(i.messageTypesWatch(),i.messageTypesWatch=null);var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var a=0;a<e.messageTypes.length;a++){var r=e.messageTypes[a];n.messageType[r]?t.push(angular.copy(n.messageType[r])):t.push({name:r,value:r})}i.messageTypes=t,i.messageTypesWatch=i.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:i}}a.$inject=["$compile","$filter","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(4);var r=n(36),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.allowedEntityTypes=[t.entityType.device,t.entityType.asset,t.entityType.tenant,t.entityType.customer,t.entityType.user,t.entityType.dashboard,t.entityType.rulechain,t.entityType.rulenode],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(37),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(38),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(39),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){function r(e){e>-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=a,t.removeKeyVal=r,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(40),o=i(r);n(5)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(41),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(42),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(85),r=i(a),o=n(87),l=i(o),s=n(88),u=i(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",r.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(43),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(44),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(92),r=i(a),o=n(78),l=i(o),s=n(72),u=i(s),d=n(86),c=i(d),m=n(52),g=i(m),p=n(68),f=i(p),b=n(84),v=i(b),y=n(67),q=i(y),h=n(83),$=i(h),x=n(91),k=i(x);t.default=angular.module("thingsboard.ruleChain.config",[r.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",$.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647'.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use <code>${metaKeyName}</code> to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use <code>${metaKeyName}</code> to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use <code>${metaKeyName}</code> to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use <code>${metaKeyName}</code> to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","request-method":"Request method",headers:"Headers","headers-hint":"Use <code>${metaKeyName}</code> in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",
+retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","message-attributes-hint":"Use <code>${metaKeyName}</code> in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-message-alarm-data":"Use message alarm data","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required."},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};e.translations("en_US",t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){(0,o.default)(e)}a.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(90),o=i(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},fetchModeType:["FIRST","LAST","ALL"],httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},timeUnit:{MILLISECONDS:{value:"MILLISECONDS",name:"tb.rulenode.time-unit-milliseconds"},SECONDS:{value:"SECONDS",name:"tb.rulenode.time-unit-seconds"},MINUTES:{value:"MINUTES",name:"tb.rulenode.time-unit-minutes"},HOURS:{value:"HOURS",name:"tb.rulenode.time-unit-hours"},DAYS:{value:"DAYS",name:"tb.rulenode.time-unit-days"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}]));
 //# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file

tools/pom.xml 1(+0 -1)

diff --git a/tools/pom.xml b/tools/pom.xml
index c1c1ded..616d894 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -23,7 +23,6 @@
         <version>2.2.1-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
-    <groupId>org.thingsboard</groupId>
     <artifactId>tools</artifactId>
     <packaging>jar</packaging>
 
diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
index 4c8c0a4..53ec22d 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -107,6 +108,27 @@ public class RestClient implements ClientHttpRequestInterceptor {
         }
     }
 
+    public Optional<JsonNode> getAttributes(String accessToken, String clientKeys, String sharedKeys) {
+        Map<String, String> params = new HashMap<>();
+        params.put("accessToken", accessToken);
+        params.put("clientKeys", clientKeys);
+        params.put("sharedKeys", sharedKeys);
+        try {
+            ResponseEntity<JsonNode> telemetryEntity = restTemplate.getForEntity(baseURL + "/api/v1/{accessToken}/attributes?clientKeys={clientKeys}&sharedKeys={sharedKeys}", JsonNode.class, params);
+            return Optional.of(telemetryEntity.getBody());
+        } catch (HttpClientErrorException exception) {
+            if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+                return Optional.empty();
+            } else {
+                throw exception;
+            }
+        }
+    }
+
+    public Customer createCustomer(Customer customer) {
+        return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
+    }
+
     public Customer createCustomer(String title) {
         Customer customer = new Customer();
         customer.setTitle(title);
@@ -120,6 +142,25 @@ public class RestClient implements ClientHttpRequestInterceptor {
         return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
     }
 
+    public DeviceCredentials updateDeviceCredentials(DeviceId deviceId, String token) {
+        DeviceCredentials deviceCredentials = getCredentials(deviceId);
+        deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
+        deviceCredentials.setCredentialsId(token);
+        return saveDeviceCredentials(deviceCredentials);
+    }
+
+    public DeviceCredentials saveDeviceCredentials(DeviceCredentials deviceCredentials) {
+        return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody();
+    }
+
+    public Device createDevice(Device device) {
+        return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
+    }
+
+    public Asset createAsset(Asset asset) {
+        return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
+    }
+
     public Asset createAsset(String name, String type) {
         Asset asset = new Asset();
         asset.setName(name);
@@ -131,6 +172,18 @@ public class RestClient implements ClientHttpRequestInterceptor {
         return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
     }
 
+    public void deleteCustomer(CustomerId customerId) {
+        restTemplate.delete(baseURL + "/api/customer/{customerId}", customerId);
+    }
+
+    public void deleteDevice(DeviceId deviceId) {
+        restTemplate.delete(baseURL + "/api/device/{deviceId}", deviceId);
+    }
+
+    public void deleteAsset(AssetId assetId) {
+        restTemplate.delete(baseURL + "/api/asset/{assetId}", assetId);
+    }
+
     public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
         return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
                 customerId.toString(), deviceId.toString()).getBody();
diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
index 4a38f71..4ac98b5 100644
--- a/ui/src/app/api/subscription.js
+++ b/ui/src/app/api/subscription.js
@@ -648,6 +648,12 @@ export default class Subscription {
     }
 
     dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
+        for (var x = 0; x < this.datasourceListeners.length; x++) {
+            this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
+            if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) {
+                this.datasources[x].dataReceived = true;
+            }
+        }
         this.notifyDataLoaded();
         var update = true;
         var currentData;
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
index 4c79f9a..fbff8d2 100644
--- a/ui/src/app/entity/entity-subtype-autocomplete.directive.js
+++ b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
@@ -62,6 +62,7 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, 
         }
 
         scope.subTypeSearchTextChanged = function() {
+            scope.subType = scope.subTypeSearchText;
         }
 
         scope.updateView = function () {
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.tpl.html b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
index 97da504..653203a 100644
--- a/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
+++ b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
@@ -28,7 +28,7 @@
                  md-min-length="0"
                  placeholder="{{ selectEntitySubtypeText | translate }}"
                  md-floating-label="{{ entitySubtypeText | translate }}"
-                 md-select-on-match="true"
+                 md-select-on-match="false"
                  md-menu-class="tb-entity-subtype-autocomplete">
     <md-item-template>
         <div class="tb-entity-subtype-item">
diff --git a/ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js b/ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js
index 2dfc961..96c398b 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js
+++ b/ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js
@@ -31,17 +31,38 @@ export default function ExtensionFormModbusDirective($compile, $templateCache, $
 
     var linker = function(scope, element) {
 
+        function TcpTransport() {
+            this.type = "tcp",
+            this.host = "localhost",
+            this.port = 502,
+            this.timeout = 5000,
+            this.reconnect = true,
+            this.rtuOverTcp = false
+        }
+
+        function UdpTransport() {
+            this.type = "udp",
+            this.host = "localhost",
+            this.port = 502,
+            this.timeout = 5000
+        }
+
+        function RtuTransport() {
+            this.type = "rtu",
+            this.portName = "COM1",
+            this.encoding = "ascii",
+            this.timeout = 5000,
+            this.baudRate = 115200,
+            this.dataBits = 7,
+            this.stopBits = 1,
+            this.parity ="even"
+        }
 
         function Server() {
-            this.transport = {
-                "type": "tcp",
-                "host": "localhost",
-                "port": 502,
-                "timeout": 3000
-            };
+            this.transport = new TcpTransport();
             this.devices = []
         }
-
+		
         function Device() {
             this.unitId = 1;
             this.deviceName = "";
@@ -105,9 +126,13 @@ export default function ExtensionFormModbusDirective($compile, $templateCache, $
         scope.onTransportChanged = function(server) {
             var type = server.transport.type;
 
-            server.transport = {};
-            server.transport.type = type;
-            server.transport.timeout = 3000;
+            if (type === "tcp") {
+                server.transport = new TcpTransport();
+            } else if (type === "udp") {
+                server.transport = new UdpTransport();
+            } else if (type === "rtu") {
+                server.transport = new RtuTransport();
+            }
 
             scope.theForm.$setDirty();
         };
diff --git a/ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html
index 989d3f0..508ddc0 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html
@@ -73,49 +73,69 @@
                                             
                                         </div>
                                         
-                                        <div layout="row" ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
-                                            <md-input-container flex="33" class="md-block">
-                                                <label translate>extension.host</label>
-                                                <input required name="transportHost_{{serverIndex}}" ng-model="server.transport.host">
-                                                <div ng-messages="theForm['transportHost_' + serverIndex].$error">
-                                                    <div translate ng-message="required">extension.field-required</div>
-                                                </div>
-                                            </md-input-container>
+                                        <div ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
+                                            <div layout="row">
+                                                <md-input-container flex="33" class="md-block">
+                                                    <label translate>extension.host</label>
+                                                    <input required name="transportHost_{{serverIndex}}" ng-model="server.transport.host">
+                                                    <div ng-messages="theForm['transportHost_' + serverIndex].$error">
+                                                        <div translate ng-message="required">extension.field-required</div>
+                                                    </div>
+                                                </md-input-container>
 
-                                            <md-input-container flex="33" class="md-block">
-                                                <label translate>extension.port</label>
-                                                <input type="number"
-                                                       required
-                                                       name="transportPort_{{serverIndex}}"
-                                                       ng-model="server.transport.port"
-                                                       min="1"
-                                                       max="65535"
-                                                >
-                                                <div ng-messages="theForm['transportPort_' + serverIndex].$error">
-                                                    <div translate
-                                                         ng-message="required"
-                                                    >extension.field-required</div>
-                                                    <div translate
-                                                         ng-message="min"
-                                                    >extension.port-range</div>
-                                                    <div translate
-                                                         ng-message="max"
-                                                    >extension.port-range</div>
-                                                </div>
-                                            </md-input-container>
-                                            
-                                            <md-input-container flex="33" class="md-block">
-                                                <label translate>extension.timeout</label>
-                                                <input type="number"
-                                                       required name="transportTimeout_{{serverIndex}}"
-                                                       ng-model="server.transport.timeout"
-                                                >
-                                                <div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
-                                                    <div translate
-                                                         ng-message="required"
-                                                    >extension.field-required</div>
-                                                </div>
-                                            </md-input-container>
+                                                <md-input-container flex="33" class="md-block">
+                                                    <label translate>extension.port</label>
+                                                    <input type="number"
+                                                           required
+                                                           name="transportPort_{{serverIndex}}"
+                                                           ng-model="server.transport.port"
+                                                           min="1"
+                                                           max="65535"
+                                                    >
+                                                    <div ng-messages="theForm['transportPort_' + serverIndex].$error">
+                                                        <div translate
+                                                             ng-message="required"
+                                                        >extension.field-required</div>
+                                                        <div translate
+                                                             ng-message="min"
+                                                        >extension.port-range</div>
+                                                        <div translate
+                                                             ng-message="max"
+                                                        >extension.port-range</div>
+                                                    </div>
+                                                </md-input-container>
+                                                
+                                                <md-input-container flex="33" class="md-block">
+                                                    <label translate>extension.timeout</label>
+                                                    <input type="number"
+                                                           required name="transportTimeout_{{serverIndex}}"
+                                                           ng-model="server.transport.timeout"
+                                                    >
+                                                    <div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
+                                                        <div translate
+                                                             ng-message="required"
+                                                        >extension.field-required</div>
+                                                    </div>
+                                                </md-input-container>
+                                            </div>
+
+                                            <div layout="row" ng-if="server.transport.type == 'tcp'">
+                                                <md-input-container flex="50" class="md-block">
+                                                    <md-checkbox aria-label="{{ 'extension.modbus-tcp-reconnect' | translate }}"
+                                                                 ng-checked="server.transport.reconnect"
+                                                                 name="transportTcpReconnect_{{serverIndex}}"
+                                                                 ng-model="server.transport.reconnect">{{ 'extension.modbus-tcp-reconnect' | translate }}
+                                                    </md-checkbox>
+                                                </md-input-container>
+
+                                                <md-input-container flex="50" class="md-block">
+                                                    <md-checkbox aria-label="{{ 'extension.modbus-rtu-over-tcp' | translate }}"
+                                                                 ng-checked="server.transport.rtuOverTcp"
+                                                                 name="transportRtuOverTcp_{{serverIndex}}"
+                                                                 ng-model="server.transport.rtuOverTcp">{{ 'extension.modbus-rtu-over-tcp' | translate }}
+                                                    </md-checkbox>
+                                                </md-input-container>
+                                            </div>
                                         </div>
                                         
                                         <div ng-if="server.transport.type == 'rtu'">
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
index 9f0b547..f972b78 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
@@ -241,14 +241,14 @@
                                                                                 </md-input-container>
                                                                                 <md-input-container ng-if="map.converter.typeExp == 'deviceTypeJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$invalid">
                                                                                     <label translate>extension.json-type-expression</label>
-                                                                                    <input required name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
+                                                                                    <input name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
                                                                                     <div ng-messages="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$error">
                                                                                         <div translate ng-message="required">extension.field-required</div>
                                                                                     </div>
                                                                                 </md-input-container>
                                                                                 <md-input-container ng-if="map.converter.typeExp == 'deviceTypeTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$invalid">
                                                                                     <label translate>extension.topic-type-expression</label>
-                                                                                    <input required name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
+                                                                                    <input name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
                                                                                     <div ng-messages="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$error">
                                                                                         <div translate ng-message="required">extension.field-required</div>
                                                                                     </div>
@@ -261,7 +261,7 @@
                                                                                 </md-input-container>
                                                                                 <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
                                                                                     <label translate>extension.filter-expression</label>
-                                                                                    <input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
+                                                                                    <input name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
                                                                                     <div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
                                                                                         <div translate ng-message="required">extension.field-required</div>
                                                                                     </div>
diff --git a/ui/src/app/locale/locale.constant-de_DE.json b/ui/src/app/locale/locale.constant-de_DE.json
new file mode 100644
index 0000000..b777b18
--- /dev/null
+++ b/ui/src/app/locale/locale.constant-de_DE.json
@@ -0,0 +1,1581 @@
+{
+  "access": {
+    "unauthorized": "Nicht autorisiert",
+    "unauthorized-access": "Unautorisierter Zugriff",
+    "unauthorized-access-text": "Sie sollten sich anmelden, um Zugriff auf diese Daten zu erhalten!",
+    "access-forbidden": "Keine Zugangsberechtigung",
+    "access-forbidden-text": "Sie haben keine Zugangsberechtigung für diesen Bereich!<br/>Versuchen Sie sich mit einem anderen Benutzer anzumelden um Zugriff auf diesen Bereich zu bekommen.",
+    "refresh-token-expired": "Sitzung ist abgelaufen",
+    "refresh-token-failed": "Sitzung kann nicht aktualisiert werden"
+  },
+  "action": {
+    "activate": "Aktivieren",
+    "suspend": "Unterbrechen",
+    "save": "Speichern",
+    "saveAs": "Speichern unter",
+    "cancel": "Abbrechen",
+    "ok": "OK",
+    "delete": "Löschen",
+    "add": "Hinzufügen",
+    "yes": "Ja",
+    "no": "Nein",
+    "update": "Aktualisieren",
+    "remove": "Löschen",
+    "search": "Suche",
+    "clear-search": "Suchanfrage löschen",
+    "assign": "Zoordnen",
+    "unassign": "Zuordnung aufheben",
+    "share": "Teilen",
+    "make-private": "Privat machen",
+    "apply": "Anwenden",
+    "apply-changes": "Änderungen übernehmen",
+    "edit-mode": "Bearbeitungsmodus",
+    "enter-edit-mode": "Zum Bearbeitungsmodus wechseln",
+    "decline-changes": "Änderungen nicht übernehmen",
+    "close": "Schließen",
+    "back": "Zurück",
+    "run": "Ausführen",
+    "sign-in": "Anmelden!",
+    "edit": "Bearbeiten",
+    "view": "Ansicht",
+    "create": "Erstellen",
+    "drag": "Ziehen",
+    "refresh": "Aktualisieren",
+    "undo": "Rückgängig machen",
+    "copy": "Kopieren",
+    "paste": "Einfügen",
+    "copy-reference": "Zeichen kopieren",
+    "paste-reference": "Zeichen einfügen",
+    "import": "Importieren",
+    "export": "Exportieren",
+    "share-via": "Teilen mit {{provider}}"
+  },
+  "aggregation": {
+    "aggregation": "Aggregation",
+    "function": "Datenaggregationsfunktion",
+    "limit": "Höchstwerte",
+    "group-interval": "Gruppierungsintervall",
+    "min": "Minimal",
+    "max": "Maximal",
+    "avg": "Durchschnitt",
+    "sum": "Summe",
+    "count": "Anzahl",
+    "none": "kein Wert"
+  },
+  "admin": {
+    "general": "Allgemein",
+    "general-settings": "Allgemeine Einstellungen",
+    "outgoing-mail": "E-Mail Versand",
+    "outgoing-mail-settings": "Konfiguration des Postausgangsservers",
+    "system-settings": "Systemeinstellungen",
+    "test-mail-sent": "Test E-Mail wurde erfolgreich versendet!",
+    "base-url": "Basis-URL",
+    "base-url-required": "Basis-URL ist erforderlich.",
+    "mail-from": "E-Mail von",
+    "mail-from-required": "E-Mail von ist erforderlich.",
+    "smtp-protocol": "SMTP Protokoll",
+    "smtp-host": "SMTP Host",
+    "smtp-host-required": "SMTP Host ist erforderlich.",
+    "smtp-port": "SMTP Port",
+    "smtp-port-required": "Sie müssen einen SMTP Port angeben.",
+    "smtp-port-invalid": "Das ist kein gültiger SMTP Port.",
+    "timeout-msec": "Wartezeit (msec)",
+    "timeout-required": "Wartezeit ist erforderlich.",
+    "timeout-invalid": "Das ist keine gültige Wartezeit.",
+    "enable-tls": "TLS aktivieren",
+    "send-test-mail": "Test E-Mail senden"
+  },
+  "alarm": {
+    "alarm": "Alarm",
+    "alarms": "Alarme",
+    "select-alarm": "Alarm auswählen",
+    "no-alarms-matching": "Keine passenden Alarme zu '{{entity}}' wurden gefunden.",
+    "alarm-required": "Alarm ist erforderlich",
+    "alarm-status": "Alarm Status",
+    "search-status": {
+      "ANY": "Jeder",
+      "ACTIVE": "Aktiv",
+      "CLEARED": "Gelöscht",
+      "ACK": "Bestätigt",
+      "UNACK": "Nicht bestätigt"
+    },
+    "display-status": {
+      "ACTIVE_UNACK": "Nicht bestätigt aktiv",
+      "ACTIVE_ACK": "Bestätigt aktiv",
+      "CLEARED_UNACK": "Nicht bestätigt",
+      "CLEARED_ACK": "Bestätigung gelöscht"
+    },
+    "no-alarms-prompt": "Keine Alarme gefunden",
+    "created-time": "Erstellungszeit",
+    "type": "Typ",
+    "severity": "Schwere",
+    "originator": "Urheber",
+    "originator-type": "Urheber-Typ",
+    "details": "Details",
+    "status": "Status",
+    "alarm-details": "Alarm-Details",
+    "start-time": "Startzeit",
+    "end-time": "Endzeit",
+    "ack-time": "Bestätigungszeit",
+    "clear-time": "Zeit gelöscht",
+    "severity-critical": "Kritisch",
+    "severity-major": "Groß",
+    "severity-minor": "Klein",
+    "severity-warning": "Warnung",
+    "severity-indeterminate": "Unbestimmt",
+    "acknowledge": "Bestätigen",
+    "clear": "Löschen",
+    "search": "Alarme Suchen",
+    "selected-alarms": "{ count, plural, 1 {1 Alarm} other {# Alarme} } ausgewählt",
+    "no-data": "Keine Daten zum Anzeigen",
+    "polling-interval": "Alarmabfrageintervall (sec)",
+    "polling-interval-required": "Alarmabfrageintervall ist erforderlich.",
+    "min-polling-interval-message": "Mindestens 1 sec Abrufintervall ist zulässig.",
+    "aknowledge-alarms-title": "{ count, plural, 1 {1 Alarm} other {# Alarme} } bestätigen",
+    "aknowledge-alarms-text": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Alarm} other {# Alarme} } bestätigen möchten?",
+    "aknowledge-alarm-title": "Alarm bestätigen",
+    "aknowledge-alarm-text": "Möchten Sie den Alarm wirklich bestätigen?",
+    "clear-alarms-title": "{ count, plural, 1 {1 Alarm} other {# Alarme} } löschen",
+    "clear-alarms-text": "Möchten Sie wirklich { count, plural, 1 {1 Alarm} other {# Alarme} } löschen?",
+    "clear-alarm-title": "Alarm löschen",
+    "clear-alarm-text": "Möchten Sie den Alarm wirklich löschen?",
+    "alarm-status-filter": "Alarm Status Filter"
+  },
+  "alias": {
+    "add": "Alias hinzufügen",
+    "edit": "Alias bearbeiten",
+    "name": "Aliasname",
+    "name-required": "Aliasname ist erforderlich",
+    "duplicate-alias": "Ein Alias mit demselben Namen ist bereits vorhanden.",
+    "filter-type-single-entity": "Einzelne Entität",
+    "filter-type-entity-list": "Entitätsliste",
+    "filter-type-entity-name": "Entitätsname",
+    "filter-type-state-entity": "Entität aus dem Dashboard Status",
+    "filter-type-state-entity-description": "Entität aus den Dashboard Status Parametern",
+    "filter-type-asset-type": "Objekttyp",
+    "filter-type-asset-type-description": "Objekte vom Typ '{{assetType}}'",
+    "filter-type-asset-type-and-name-description": "Objekte vom Typ '{{assetType}}' und Name beginnend mit '{{prefix}}'",
+    "filter-type-device-type": "Gerätetyp",
+    "filter-type-device-type-description": "Geräte vom Typ '{{deviceType}}'",
+    "filter-type-device-type-and-name-description": "Geräte vom Typ '{{deviceType}}' und Name beginnend mit '{{prefix}}'",
+    "filter-type-entity-view-type": "Entitätsansichtstyp",
+    "filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityView}}'",
+    "filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityView}}' und Name beginnend mit '{{prefix}}'",
+    "filter-type-relations-query": "Beziehungsabfrage",
+    "filter-type-relations-query-description": "{{entities}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}",
+    "filter-type-asset-search-query": "Objektabfrage",
+    "filter-type-asset-search-query-description": "Objekte vom Typ {{assetTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}",
+    "filter-type-device-search-query": "Geräteabfrage",
+    "filter-type-device-search-query-description": "Geräte vom Typ {{deviceTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}",
+    "filter-type-entity-view-search-query": "Entitätsansichtsabfrage",
+    "filter-type-entity-view-search-query-description": "Entitätsansichten vom Typ {{entityViewTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}",
+    "entity-filter": "Entitätsfilter",
+    "resolve-multiple": "Als mehrere Entitäten auflösen",
+    "filter-type": "Filtertyp",
+    "filter-type-required": "Filtertyp ist erforderlich.",
+    "entity-filter-no-entity-matched": "Es wurden keine Entitäten gefunden, die dem angegebenen Filter entsprechen.",
+    "no-entity-filter-specified": "Es wurde kein Entitätsfilter angegeben",
+    "root-state-entity": "Dashboard Status Entität als Wurzel verwenden",
+    "root-entity": "Wurzelentität",
+    "state-entity-parameter-name": "Parameter-Name der Statusentität",
+    "default-state-entity": "Standard Statusentität",
+    "default-entity-parameter-name": "Standartmäßig",
+    "max-relation-level": "Maximale Beziehungstiefe",
+    "unlimited-level": "Unbegrenzte Ebenen",
+    "state-entity": "Dashboard Status Entität",
+    "all-entities": "Alle Entitäten",
+    "any-relation": "Jede Beziehung"
+  },
+  "asset": {
+    "asset": "Objekt",
+    "assets": "Objekte",
+    "management": "Objektverwaltung",
+    "view-assets": "Objekte anzeigen",
+    "add": "Objekt hinzufügen",
+    "assign-to-customer": "Einem Kunden zuordnen",
+    "assign-asset-to-customer": "Objekte dem Kunden zuordnen",
+    "assign-asset-to-customer-text": "Bitte wählen Sie die Objekte aus, die dem Kunden zugeordnet werden sollen",
+    "no-assets-text": "Keine Objekte gefunden",
+    "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Objekte zugeordnet werden sollen",
+    "public": "Öffentlich",
+    "assignedToCustomer": "Kunden zugeordnet",
+    "make-public": "Objekt öffentlich machen",
+    "make-private": "Objekt privat machen",
+    "unassign-from-customer": "Kundenzuordnung aufheben",
+    "delete": "Objekt löschen",
+    "asset-public": "Objekt ist öffentlich",
+    "asset-type": "Objekttyp",
+    "asset-type-required": "Objekttyp ist erforderlich.",
+    "select-asset-type": "Objekttyp auswählen",
+    "enter-asset-type": "Objekttyp bestätigen",
+    "any-asset": "Jedes Objekt",
+    "no-asset-types-matching": "Es wurden keine zu '{{entitySubtype}}' passenden Objekte gefunden.",
+    "asset-type-list-empty": "Keine Objekttypen ausgewählt.",
+    "asset-types": "Objekttypen",
+    "name": "Name",
+    "name-required": "Name ist erforderlich.",
+    "description": "Beschreibung",
+    "type": "Typ",
+    "type-required": "Typ ist erforderlich.",
+    "details": "Details",
+    "events": "Ereignisse",
+    "add-asset-text": "Neues Objekt hinzufügen",
+    "asset-details": "Objektdetails",
+    "assign-assets": "Objekte zuordnen",
+    "assign-assets-text": "Kunden { count, plural, 1 {1 Objekt} other {# Objekte} } zuordnen",
+    "delete-assets": "Objekte löschen",
+    "unassign-assets": "Objektzuordnungen aufheben",
+    "unassign-assets-action-title": "Kunden { count, plural, 1 {1 Objektzuordnung} other {# Objektzuordnungen} } aufheben",
+    "assign-new-asset": "Neues Objekt zuordnen",
+    "delete-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' löschen möchten?",
+    "delete-asset-text": "Vorsicht, nach Bestätigung wird das Objekt und alle zugehörigen Daten nicht wiederherstellbar gelöscht.",
+    "delete-assets-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Objekt} other {# Objekte} } löschen möchten?",
+    "delete-assets-action-title": "{ count, plural, 1 {1 Objekt} other {# Objekte} } löschen",
+    "delete-assets-text": "Vorsicht, nach Bestätigung werden die ausgewählten Objekte und alle zugehörigen Daten nicht wiederherstellbar gelöscht",
+    "make-public-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' öffentlich machen möchten?",
+    "make-public-asset-text": "Nach Bestätigung wird das Objekt und alle zugehörigen Daten anderen zugänglich gemacht.",
+    "make-private-asset-title": "Sind Sie sicher, dass Sie das Objekt '{{assetName}}' privat machen möchten?",
+    "make-private-asset-text": "Nach Bestätigung wird das Objekt und alle zugehörigen Daten privat und ist für andere nicht mehr zugänglich.",
+    "unassign-asset-title": "Sind Sie sicher, dass Sie die Zuordnung für das Objekt '{{assetName}}' aufheben möchten?",
+    "unassign-asset-text": "Nach Bestätigung wird die Zuordnung des Objekts aufgehoben und es ist für den Kunden nicht mehr zugänglich.",
+    "unassign-asset": "Zuordnung des Objekts aufheben",
+    "unassign-assets-title": "Möchten Sie die Zuordnung von { count, plural, 1 {1 Objekt} other {# Objekte} } aufheben?",
+    "unassign-assets-text": "Nach Bestätigung wird die Zuordnung der ausgewählten Objekte aufgehoben und sie sind für den Kunden nicht mehr zugänglich.",
+    "copyId": "Objekt-ID kopieren",
+    "idCopiedMessage": "Objekt-ID wurde in die Zwischenablage kopiert",
+    "select-asset": "Objekt auswählen",
+    "no-assets-matching": "Es wurden keine zu '{{entity}}' passenden Objekte gefunden.",
+    "asset-required": "Objekt ist erforderlich",
+    "name-starts-with": "Name des Objekts beginnt mit"
+  },
+  "attribute": {
+    "attributes": "Eigenschaften",
+    "latest-telemetry": "Neueste Telemetrie",
+    "attributes-scope": "Entitätseigenschaftsbereich",
+    "scope-latest-telemetry": "Neueste Telemetrie",
+    "scope-client": "Client Eigenschaften",
+    "scope-server": "Server Eigenschaften",
+    "scope-shared": "Gemeinsame Eigenschaften",
+    "add": "Eigenschaft hinzufügen",
+    "key": "Schlüssel",
+    "last-update-time": "Datum der letzten Aktualisierung",
+    "key-required": "Eigenschaftsschlüssel ist erforderlich.",
+    "value": "Wert",
+    "value-required": "Eigenschaftswert ist erforderlich.",
+    "delete-attributes-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Eigenschaft} other {# Eigenschaften} } löschen möchten?",
+    "delete-attributes-text": "Seien Sie vorsichtig, nach der Bestätigung werden alle ausgewählten Eigenschaften entfernt.",
+    "delete-attributes": "Eigenschaften löschen",
+    "enter-attribute-value": "Geben Sie den Eigenschaftswert ein",
+    "show-on-widget": "Im Widget anzeigen",
+    "widget-mode": "Widget-Modus",
+    "next-widget": "Nächstes Widget",
+    "prev-widget": "Vorheriges Widget",
+    "add-to-dashboard": "Zum Dashboard hinzufügen",
+    "add-widget-to-dashboard": "Widget zum Dashboard hinzufügen",
+    "selected-attributes": "{ count, plural, 1 {1 Eigenschaft} other {# Eigenschaften} } ausgewählt",
+    "selected-telemetry": "{ count, plural, 1 {1 Telemetrieeinheit } other {# Telemetrieeinheiten} } ausgewählt"
+  },
+  "audit-log": {
+    "audit": "Audit",
+    "audit-logs": "Audit-Protokolle",
+    "timestamp": "Zeitstempel",
+    "entity-type": "Entitätstype",
+    "entity-name": "Entitätsname",
+    "user": "User",
+    "type": "Typ",
+    "status": "Status",
+    "details": "Details",
+    "type-added": "Hinzugefügt",
+    "type-deleted": "Gelöscht",
+    "type-updated": "Aktualisiert",
+    "type-attributes-updated": "Eigenschaften aktualisiert",
+    "type-attributes-deleted": "Eigenschaften gelöscht",
+    "type-rpc-call": "RPC Aufruf",
+    "type-credentials-updated": "Anmeldeinformationen wurden aktualisiert",
+    "type-assigned-to-customer": "Kunden Zuordnung",
+    "type-unassigned-from-customer": "Kunden Zuordnung aufgehoben",
+    "type-activated": "Aktiviert",
+    "type-suspended": "Ausgesetzt",
+    "type-credentials-read": "Anmeldeinformationen gelesen",
+    "type-attributes-read": "Eigenschaften gelesen",
+    "type-relation-add-or-update": "Beziehung aktualisiert",
+    "type-relation-delete": "Beziehung gelöscht",
+    "type-relations-delete": "Alle Beziehungen gelöscht",
+    "type-alarm-ack": "Bestätigt",
+    "type-alarm-clear": "Gelöscht",
+    "status-success": "Erfolg",
+    "status-failure": "Fehler",
+    "audit-log-details": "Audit-Protokoll Details",
+    "no-audit-logs-prompt": "Keine Protokolle gefunden",
+    "action-data": "Aktionsdaten",
+    "failure-details": "Fehlerdetails",
+    "search": "Audit-Protokolle durchsuchen",
+    "clear-search": "Suche leeren"
+  },
+  "confirm-on-exit": {
+    "message": "Sie haben nicht gespeicherte Änderungen. Möchten Sie diese Seite wirklich verlassen?",
+    "html-message": "Sie haben nicht gespeicherte Änderungen.<br/>Möchten Sie diese Seite wirklich verlassen?",
+    "title": "Nicht gespeicherte Änderungen"
+  },
+  "contact": {
+    "country": "Land",
+    "city": "Stadt",
+    "state": "Bundesland",
+    "postal-code": "Postleitzahl",
+    "postal-code-invalid": "Ungültiges Format der Postleitzahl.",
+    "address": "Adresse",
+    "address2": "Adresse 2",
+    "phone": "Telefon",
+    "email": "E-Mail",
+    "no-address": "Keine Adresse"
+  },
+  "common": {
+    "username": "Benutzername",
+    "password": "Passwort",
+    "enter-username": "Benutzername eingeben",
+    "enter-password": "Passwort eingeben",
+    "enter-search": "Suche eingeben"
+  },
+  "content-type": {
+    "json": "Json",
+    "text": "Text",
+    "binary": "Binär (Base64)"
+  },
+  "customer": {
+    "customer": "Kunde",
+    "customers": "Kunden",
+    "management": "Kundenverwaltung",
+    "dashboard": "Kunden Dashboard",
+    "dashboards": "Kunden Dashboards",
+    "devices": "Kundengeräte",
+    "entity-views": "Kunden Entitätsansichten",
+    "assets": "Kundenobjekte",
+    "public-dashboards": "Öffentliche Dashboards",
+    "public-devices": "Öffentliche Geräte",
+    "public-assets": "Öffentliche Objekte",
+    "public-entity-views": "Öffentliche Entitätsansichten",
+    "add": "Kunde hinzufügen",
+    "delete": "Kunde löschen",
+    "manage-customer-users": "Kundenbenutzer verwalten",
+    "manage-customer-devices": "Kundengeräte verwalten",
+    "manage-customer-dashboards": "Kunden-Dashboards verwalten",
+    "manage-public-devices": "Öffentliche Geräte verwalten",
+    "manage-public-dashboards": "Öffentliche Dashboards verwalten",
+    "manage-customer-assets": "Kundenobjekte verwalten",
+    "manage-public-assets": "Öffentliche Objekte verwalten",
+    "add-customer-text": "Neuen Kunden hinzufügen",
+    "no-customers-text": "Keine Kunden gefunden",
+    "customer-details": "Kundendetails",
+    "delete-customer-title": "Sind Sie sicher, dass der Kunde '{{customerTitle}}' gelöscht werden soll?",
+    "delete-customer-text": "Vorsicht, nach Bestätigung wird der Kunde und alle zugehörigen Daten nicht wiederherstellbar gelöscht.",
+    "delete-customers-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Kunde} other {# Kunden} } löschen möchten?",
+    "delete-customers-action-title": "{ count, plural, 1 {1 Kunde} other {# Kunden} } löschen",
+    "delete-customers-text": "Seien Sie vorsichtig, nach der Bestätigung werden alle ausgewählten Kunden und alle zugehörigen Daten nicht wiederherstellbar gelöscht.",
+    "manage-users": "User verwalten",
+    "manage-assets": "Objekte verwalten",
+    "manage-devices": "Geräte verwalten",
+    "manage-dashboards": "Dashboards verwalten",
+    "title": "Titel",
+    "title-required": "Titel ist erforderlich.",
+    "description": "Beschreibung",
+    "details": "Details",
+    "events": "Ereignisse",
+    "copyId": "Kunden-ID kopieren",
+    "idCopiedMessage": "Kunden-ID wurde in die Zwischenablage kopiert",
+    "select-customer": "Kunden auswählen",
+    "no-customers-matching": "Keine Kunden für '{{entity}}' gefunden.",
+    "customer-required": "Kunde ist erforderlich",
+    "select-default-customer": "Wählen Sie den Standardkunden aus.",
+    "default-customer": "Standardkunde",
+    "default-customer-required": "Ein Standardkunde ist erforderlich, um das Dashboard auf Mandantenebene zu testen."
+  },
+  "datetime": {
+    "date-from": "Datum von",
+    "time-from": "Zeit von",
+    "date-to": "Datum bis",
+    "time-to": "Zeit bis"
+  },
+  "dashboard": {
+    "dashboard": "Dashboard",
+    "dashboards": "Dashboards",
+    "management": "Dashboardverwaltung",
+    "view-dashboards": "Dashboards anzeigen",
+    "add": "Dashboard hinzufügen",
+    "assign-dashboard-to-customer": "Dashboard(s) dem Kunden zuordnen",
+    "assign-dashboard-to-customer-text": "Bitte wählen Sie die Dashboards aus, die Sie dem Kunden zuordnen möchten",
+    "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Dashboards zugeordnet werden sollen",
+    "assign-to-customer": "Kunden zuordnen",
+    "unassign-from-customer": "Zuordnung zum Kunden aufheben",
+    "make-public": "Dashboard öffentlich machen",
+    "make-private": "Dashboard privat machen",
+    "manage-assigned-customers": "Zugeordnete Kunden verwalten",
+    "assigned-customers": "Zugeordnete Kunden",
+    "assign-to-customers": "Dashboard(s) zu Kunden zuweisen",
+    "assign-to-customers-text": "Bitte wählen Sie den Kunden aus, dem Sie das Dashboard(s) zuweisen möchten",
+    "unassign-from-customers": "Zuordnung von Dashboard(s) zu Kunden aufheben",
+    "unassign-from-customers-text": "Bitte wählen Sie die Kunden aus, für die die Zuordnung von Dashboard(s) aufgehoben werden soll",
+    "no-dashboards-text": "Keine Dashboard(s) gefunden",
+    "no-widgets": "Keine Widgets konfiguriert",
+    "add-widget": "Neues Widget hinzufügen",
+    "title": "Titel",
+    "select-widget-title": "Widget auswählen",
+    "select-widget-subtitle": "Liste der verfügbaren Widget-Typen",
+    "delete": "Dashboard löschen",
+    "title-required": "Titel ist erforderlich.",
+    "description": "Beschreibung",
+    "details": "Details",
+    "dashboard-details": "Dashboard-Details",
+    "add-dashboard-text": "Neues Dashboard hinzufügen",
+    "assign-dashboards": "Dashboards zuweisen",
+    "assign-new-dashboard": "Neues Dashboard zuweisen",
+    "assign-dashboards-text": "Zuordnen { count, plural, 1 {1 Dashboard} other {# Dashboards} } zu Kunden",
+    "unassign-dashboards-action-text": "Zuordnung { count, plural, 1 {1 Dashboard} other {# Dashboards} } vom Kunden aufheben",
+    "delete-dashboards": "Dashboards löschen",
+    "unassign-dashboards": "Zuordnung von Dashboards aufheben",
+    "unassign-dashboards-action-title": "Zuordnung { count, plural, 1 {1 Dashboard} other {# Dashboards} } vom Kunden aufheben",
+    "delete-dashboard-title": "Sind Sie sicher, dass Sie das Dashboard '{{dashboardTitle}}' löschen möchten?",
+    "delete-dashboard-text": "Vorsicht, nach Bestätigung werden das Dashboard und alle zugehörigen Daten nicht mehr wiederhergestellt.",
+    "delete-dashboards-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Dashboard} other {# Dashboards} } löschen möchten?",
+    "delete-dashboards-action-title": "Löschen { count, plural, 1 {1 Dashboard} other {# Dashboards} }",
+    "delete-dashboards-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Dashboards entfernt und alle zugehörigen Daten nicht mehr wiederhergestellt.",
+    "unassign-dashboard-title": "Sind Sie sicher, dass Sie die Zuordnung zum Dashboard '{{dashboardTitle}}' aufheben möchten?",
+    "unassign-dashboard-text": "Nach der Bestätigung wird die Zuordnung des Dashboards aufgehoben und es ist für den Kunden nicht mehr zugänglich.",
+    "unassign-dashboard": "Zuordnung zum Kunden aufheben",
+    "unassign-dashboards-title": "Sind Sie sicher, dass Sie die Zuordug aufheben möchten { count, plural, 1 {1 Dashboard} other {# Dashboards} }?",
+    "unassign-dashboards-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Dashboards aufgehoben und sie sind für den Kunden nicht mehr zugänglich.",
+    "public-dashboard-title": "Dashboard wurde veröffentlicht",
+    "public-dashboard-text": "Ihr Dashboard <b>{{dashboardTitle}}</b> ist jetzt öffentlich und über nächste Öffentlichkeit zugänglich <a href='{{publicLink}}' target='_blank'>link</a>:",
+    "public-dashboard-notice": "<b>Note:</b> Vergessen Sie nicht, verwandte Geräte öffentlich zu machen, um auf Ihre Daten zugreifen zu können.",
+    "make-private-dashboard-title": "Sind Sie sicher, dass Sie das Dashboard '{{dashboardTitle}}' privatisieren möchten?",
+    "make-private-dashboard-text": "Nach der Bestätigung wird das Dashboard privatisiert und ist für andere nicht zugänglich.",
+    "make-private-dashboard": "Dashboard privatisieren",
+    "socialshare-text": "'{{dashboardTitle}}' Bereitgestellt vom ThingsBoard",
+    "socialshare-title": "'{{dashboardTitle}}' Bereitgestellt vom ThingsBoard",
+    "select-dashboard": "Dashboard auswählen",
+    "no-dashboards-matching": "Es wurden keine passenden Dashboards '{{entity}}' gefunden.",
+    "dashboard-required": "Dashboard ist erforderlich.",
+    "select-existing": "Existierendes Dashboard auswählen",
+    "create-new": "Neues Dashboard erstellen",
+    "new-dashboard-title": "Neuer Dashboard Titel",
+    "open-dashboard": "Dashboard öffnen",
+    "set-background": "Hintergrund einstellen",
+    "background-color": "Hintergrundfarbe",
+    "background-image": "Hintergrundbild",
+    "background-size-mode": "Hintergrundgrößenmodus",
+    "no-image": "Kein Bild ausgewählt",
+    "drop-image": "Legen Sie ein Bild ab oder klicken Sie, um eine hochzuladende Datei auszuwählen.",
+    "settings": "Einstellungen",
+    "columns-count": "Spalten zählen",
+    "columns-count-required": "Die Anzahl der Spalten ist erforderlich.",
+    "min-columns-count-message": "Es müssen mindestens 10 Spalten vorhanden sein.",
+    "max-columns-count-message": "Es sind maximal 100 Spalten zulässig.",
+    "widgets-margins": "Abstand zwischen den Widgets",
+    "horizontal-margin": "Horizontaler Abstand",
+    "horizontal-margin-required": "Horizontaler Abstandswert ist erforderlich.",
+    "min-horizontal-margin-message": "Der horizontale Abstandswert muss mindestens 0 betragen.",
+    "max-horizontal-margin-message": "Der horizontale Abstandswert beträgt maximal 50.",
+    "vertical-margin": "Vertikaler Abstand",
+    "vertical-margin-required": "Vertikaler Abstandswert ist erforderlich.",
+    "min-vertical-margin-message": "Der vertikale Abstandswert muss mindestens 0 betragen.",
+    "max-vertical-margin-message": "Der vertikale Abstandswert beträgt maximal 50.",
+    "autofill-height": "Layouthöhe automatisch füllen",
+    "mobile-layout": "Mobile Layouteinstellungen",
+    "mobile-row-height": "Mobile Zeilenhöhe, px",
+    "mobile-row-height-required": "Ein mobiler Zeilenhöhenwert ist erforderlich.",
+    "min-mobile-row-height-message": "Der Mindestwert für die mobile Zeilenhöhe beträgt 5 Pixel.",
+    "max-mobile-row-height-message": "Der Höchstwert für die mobile Zeilenhöhe beträgt 200 Pixel.",
+    "display-title": "Display Dashboard Titel",
+    "toolbar-always-open": "Werkzeugleiste geöffnet lassen",
+    "title-color": "Titelfarbe ",
+    "display-dashboards-selection": "Auswahl der Dashboards anzeigen",
+    "display-entities-selection": "Auswahl der Einheiten zulassen",
+    "display-dashboard-timewindow": "Zeitfenster anzeigen",
+    "display-dashboard-export": "Export anzeigen",
+    "import": "Dashboard importieren",
+    "export": "Dashboard exportieren",
+    "export-failed-error": "Dashboard kann nicht exportiert werden: {{error}}",
+    "create-new-dashboard": "Neues Dashboard erstellen",
+    "dashboard-file": "Dashboard-Datei",
+    "invalid-dashboard-file-error": "Dashboard kann nicht importiert werden: Ungültige Dashboard-Datenstruktur.",
+    "dashboard-import-missing-aliases-title": "Konfigurieren Sie die von importierten Dashboards verwendeten Aliasnamen",
+    "create-new-widget": "Neues Widget erstellen",
+    "import-widget": "Widget importieren",
+    "widget-file": "Widget-Datei",
+    "invalid-widget-file-error": "Widget kann nicht importiert werden: Ungültige Widget-Datenstruktur.",
+    "widget-import-missing-aliases-title": "Konfigurieren Sie die von importierten Widgets verwendeten Aliase",
+    "open-toolbar": "Dashboard-Werkzeugleiste öffnen",
+    "close-toolbar": "Werkzeugleiste schließen",
+    "configuration-error": "Konfigurationsfehler",
+    "alias-resolution-error-title": "Konfigurationsfehler für Dashboard-Aliasnamen",
+    "invalid-aliases-config": "Es konnten keine Geräte gefunden werden, die mit dem Aliase-Filter übereinstimmen.<br/>Bitte wenden Sie sich an Ihren Administrator, um dieses problem zu beheben.",
+    "select-devices": "Geräte auswählen",
+    "assignedToCustomer": "Dem Kunden zugewiesen",
+    "assignedToCustomers": "Kunden zugwiesen",
+    "public": "Öffentlich",
+    "public-link": "Öffentlicher Link",
+    "copy-public-link": "Öffentlichen Link kopieren",
+    "public-link-copied-message": "Der öffentliche Link des Dashboards wurde in die Zwischenablage kopiert",
+    "manage-states": "Dashboard-Status verwalten",
+    "states": "Dashboard-Status",
+    "search-states": "Dashboard-Status suchen",
+    "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } ausgewählt",
+    "edit-state": "Dashboard-Status bearbeiten",
+    "delete-state": "Dashboard-Status löschen",
+    "add-state": "Dashboard-Status hinzufügen",
+    "state": "Dashboard-Status",
+    "state-name": "Name",
+    "state-name-required": "Name des Dashboard-Status ist erforderlich.",
+    "state-id": "Status-Id",
+    "state-id-required": "Dashboard-Status-ID ist erforderlich.",
+    "state-id-exists": "Dashboard-Status mit derselben ID ist bereits vorhanden .",
+    "is-root-state": "Grundzustand",
+    "delete-state-title": "Dashboard-Status löschen",
+    "delete-state-text": "Sind Sie sicher, dass Sie den Dashboard-Status '{{stateName}}' löschen möchten?",
+    "show-details": "Details anzeigen",
+    "hide-details": "Details ausblenden",
+    "select-state": "Soll-Zustand auswählen",
+    "state-controller": "Zustandssteuerung"
+  },
+  "datakey": {
+    "settings": "Einstellungen",
+    "advanced": "Erweitert",
+    "label": "Bezeichnung",
+    "color": "Farbe",
+    "units": "Maßeinheit die neben dem Wert angezeigt wird",
+    "decimals": "Anzahl der Stellen nach dem Komma",
+    "data-generation-func": "Daten generieren",
+    "use-data-post-processing-func": "Datenverarbeitungsfunktion verwenden",
+    "configuration": "Datenschlüsselkonfiguration",
+    "timeseries": "Zeitreihe",
+    "attributes": "Eigenschaften",
+    "alarm": "Alarmfelder",
+    "timeseries-required": "Entity-Zeitreihen sind erforderlich.",
+    "timeseries-or-attributes-required": "Entity-Zeitreihen/Eigenschaften sind erforderlich.",
+    "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 Zeitreihe/Eigenschaft ist erlaubt} other {# Zeitreihen/Eigenschaften sind erlaubt} }.",
+    "alarm-fields-required": "Alarmfelder sind erforderlich.",
+    "function-types": "Funktionsarten",
+    "function-types-required": "Funktionstypen sind erforderlich.",
+    "maximum-function-types": "Maximal { count, plural, 1 {1 Funktionstyp ist erlaubt} other {# Funktionstypen sind erlaubt} }.",
+    "time-description": "Zeitstempel des aktuellen Wertes;",
+    "value-description": "Der aktuelle Wert;",
+    "prev-value-description": "Ergebnis des vorherigen Funktionsaufrufs;",
+    "time-prev-description": "Zeitmarke des vorherigen Wertes;",
+    "prev-orig-value-description": "Ursprünglicher vorheriger Wert;"
+  },
+  "datasource": {
+    "type": "Datenquellentyp",
+    "name": "Name",
+    "add-datasource-prompt": "Bitte Datenquelle hinzufügen"
+  },
+  "details": {
+    "edit-mode": "Bearbeitungsmodus",
+    "toggle-edit-mode": "Bearbeitungsmodus umschalten"
+  },
+  "device": {
+    "device": "Gerät",
+    "device-required": "Gerät ist erforderlich.",
+    "devices": "Geräte",
+    "management": "Geräte verwalten",
+    "view-devices": "Geräte anzeigen",
+    "device-alias": "Geräte-Alias",
+    "aliases": "Gerätealiasnamen",
+    "no-alias-matching": "'{{alias}}' nicht gefunden.",
+    "no-aliases-found": "Keine Aliase gefunden.",
+    "no-key-matching": "'{{key}}' nicht gefunden.",
+    "no-keys-found": "Keine Schlüssel gefunden.",
+    "create-new-alias": "Neues Alias erstellen!",
+    "create-new-key": "Neuen Schlüssel erstellen!",
+    "duplicate-alias-error": "Doppelter Alias gefunden '{{alias}}'.<br>Gerätealiasnamen müssen innerhalb des Dashboards eindeutig sein.",
+    "configure-alias": "Alias '{{alias}}' konfigurieren",
+    "no-devices-matching": "Keine passenden Geräte '{{entity}}' gefunden.",
+    "alias": "Alias",
+    "alias-required": "Geräte-Alias ist erforderlich.",
+    "remove-alias": "Geräte-Alias entfernen",
+    "add-alias": "Geräte-Alias hinzufügen",
+    "name-starts-with": "Gerätename beginnt mit",
+    "device-list": "Geräteliste",
+    "use-device-name-filter": "Filter verwenden",
+    "device-list-empty": "Keine Geräte ausgewählt.",
+    "device-name-filter-required": "Der Gerätenamefilter ist erforderlich.",
+    "device-name-filter-no-device-matched": "Keine Geräte beginnend mit '{{device}}' gefunden.",
+    "add": "Gerät hinzufügen",
+    "assign-to-customer": "Kunden zuordnen",
+    "assign-device-to-customer": "Gerät(e) dem Kunden zuordnen",
+    "assign-device-to-customer-text": "Bitte wählen Sie die Geräte aus, die Sie dem Kunden zuordnen möchten",
+    "make-public": "Gerät veröffentlichen",
+    "make-private": "Gerät privatisieren",
+    "no-devices-text": "Keine Geräte gefunden",
+    "assign-to-customer-text": "Bitte wählen Sie einen Kunden aus, dem die Geräte zugeordnet werden sollen",
+    "device-details": "Gerätedetails",
+    "add-device-text": "Neues Gerät hinzufügen",
+    "credentials": "Zugangsdaten",
+    "manage-credentials": "Zugangsdaten verwalten",
+    "delete": "Gerät löschen",
+    "assign-devices": "Gerät zuordnen",
+    "assign-devices-text": "{ count, plural, 1 {1 Gerät} other {# Geräte} } dem Kunden zuordnen",
+    "delete-devices": "Geräte löschen",
+    "unassign-from-customer": "Zuordnung zum Kunden aufheben",
+    "unassign-devices": "Nicht zugeordnete Geräte",
+    "unassign-devices-action-title": "Zuordnung von { count, plural, 1 {1 Gerät} other {# Geräte} } zum Kunden aufheben",
+    "assign-new-device": "Neues Gerät zuordnen",
+    "make-public-device-title": "Sind Sie sicher, dass Sie das Gerät  '{{deviceName}}' öffentlich machen möchten?",
+    "make-public-device-text": "Nach der Bestätigung werden das Gerät und dessen Daten öffentlich und für andere zugänglich.",
+    "make-private-device-title": "Sind Sie sicher, dass Sie das Gerät  '{{deviceName}}' privat machen möchten?",
+    "make-private-device-text": "Nach der Bestätigung werden das Gerät und dessen Daten privat und sind für andere nicht mehr zugänglich.",
+    "view-credentials": "Zugangsdaten anzeigen",
+    "delete-device-title": "Möchten Sie das Gerät '{{deviceName}}' wirklich löschen?",
+    "delete-device-text": "Vorsicht, nach Bestätigung werden das Gerät und alle zugehörigen Daten nicht mehr wiederhergestellt.",
+    "delete-devices-title": "Sind Sie sicher, dass Sie löschen möchten { count, plural, 1 {1 Gerät} other {# Geräte} }?",
+    "delete-devices-action-title": "Löschen { count, plural, 1 {1 Gerät} other {# Geräte} }",
+    "delete-devices-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Geräte entfernt und alle zugehörigen Daten werden nicht mehr wiederhergestellt.",
+    "unassign-device-title": "Sind Sie sicher, dass Sie die Zuordnung zum Gerät '{{deviceName}}' wirklich aufheben möchten?",
+    "unassign-device-text": "Nach der Bestätigung ist das Gerät nicht zugeordnet und für den Kunden nicht zugänglich.",
+    "unassign-device": "Nicht zugeordnete Geräte",
+    "unassign-devices-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Gerät} other {# Geräte} } nicht mehr zuordnen möchten?",
+    "unassign-devices-text": "Nach der Bestätigung werden alle ausgewählten Geräte nicht zugewiesen und sind für den Kunden nicht zugänglich.",
+    "device-credentials": "Geräte Zugangsdaten",
+    "credentials-type": "Art der Zugangsdaten",
+    "access-token": "Zugangs-Token",
+    "access-token-required": "Zugangs-Token ist erforderlich.",
+    "access-token-invalid": "Die Länge des Zugangs-Tokens muss zwischen 1 und 20 Zeichen betragen.",
+    "rsa-key": "RSA öffentlicher Schlüssel",
+    "rsa-key-required": "RSA öffentlicher Schlüssel ist erforderlich.",
+    "secret": "Geheimnis",
+    "secret-required": "Geheimnis ist erforderlich.",
+    "device-type": "Gerätetyp",
+    "device-type-required": "Gerätetyp ist erforderlich.",
+    "select-device-type": "Gerätetyp auswählen",
+    "enter-device-type": "Gerätetyp eingeben",
+    "any-device": "Jedes Gerät",
+    "no-device-types-matching": "Keine passenden Gerätetypen '{{entitySubtype}}' gefunden.",
+    "device-type-list-empty": "Kein Gerätetyp ausgewählt.",
+    "device-types": "Gerätetypen",
+    "name": "Name",
+    "name-required": "Name ist erforderlich.",
+    "description": "Beschreibung",
+    "events": "Ereignisse",
+    "details": "Details",
+    "copyId": "Geräte-ID kopieren",
+    "copyAccessToken": "Zugangs-Token kopieren",
+    "idCopiedMessage": "Geräte-ID wurde in die Zwischenablage kopiert",
+    "accessTokenCopiedMessage": "Geräte-Zugangs-Token wurde in die Zwischenablage kopiert",
+    "assignedToCustomer": "Dem Kunden zuordnen",
+    "unable-delete-device-alias-title": "Geräte-Alias kann nicht gelöscht werden",
+    "unable-delete-device-alias-text": "Geräte-Alias '{{deviceAlias}}' kann nicht gelöscht werden, da er von den folgenden Widgets verwendet wird:<br/>{{widgetsList}}",
+    "is-gateway": "Ist ein Gateway",
+    "public": "Öffentlich",
+    "device-public": "Gerät ist öffentlich",
+    "select-device": "Gerät auswählen"
+  },
+  "dialog": {
+    "close": "Dialog schließen"
+  },
+  "error": {
+    "unable-to-connect": "Es konnte keine Verbindung zum Server hergestellt werden! Bitte überprüfen Sie Ihre Internetverbindung.",
+    "unhandled-error-code": "Unbehandelter Fehlercode: {{errorCode}}",
+    "unknown-error": "Unbekannter Fehler"
+  },
+  "entity": {
+    "entity": "Entität",
+    "entities": "Entitäten",
+    "aliases": "Entitäts-Aliasnamen",
+    "entity-alias": "Entitätsalias",
+    "unable-delete-entity-alias-title": "Alias der Entität kann nicht gelöscht werden",
+    "unable-delete-entity-alias-text": "Alias der Entität '{{entityAlias}}' kann nicht gelöscht werden, da es von den folgenden Widget(s) verwendet wird:<br/>{{widgetsList}}",
+    "duplicate-alias-error": "Doppelte Alias gefunden '{{alias}}'.<br>Die Aliase der Entität müssen innerhalb des Dashboards eindeutig sein.",
+    "missing-entity-filter-error": "Fehlender Filter für Alias '{{alias}}'.",
+    "configure-alias": "Alias '{{alias}}' konfigurieren",
+    "alias": "Alias",
+    "alias-required": "Alias der Entität ist erforderlich.",
+    "remove-alias": "Alias der Entität entfernen",
+    "add-alias": "Alias der Entität erforderlich",
+    "entity-list": "Entitätsliste",
+    "entity-type": "Entitätstyp",
+    "entity-types": "Entitätstypen",
+    "entity-type-list": "Liste der Entitätstyp",
+    "any-entity": "Jede Entität",
+    "enter-entity-type": "Entitätstyp eingeben",
+    "no-entities-matching": "Keine passenden Entitäten für '{{entity}}' gefunden.",
+    "no-entity-types-matching": "Keine passende Entitätstypen für '{{entityType}}' gefunden.",
+    "name-starts-with": "Name beginnt mit",
+    "use-entity-name-filter": "Filter verwenden",
+    "entity-list-empty": "Keine Entitäten ausgewählt.",
+    "entity-type-list-empty": "Keine Entitättypen ausgewählt.",
+    "entity-name-filter-required": "Entitätsnamenfilter ist erforderlich.",
+    "entity-name-filter-no-entity-matched": "Keine Entitäten beginnend mit '{{entity}}' gefunden.",
+    "all-subtypes": "Alle",
+    "select-entities": "Entitäten auswählen",
+    "no-aliases-found": "Keine Aliase gefunden.",
+    "no-alias-matching": "'{{alias}}' nicht gefunden.",
+    "create-new-alias": "Erstellen Sie einen neuen Alias!",
+    "key": "Schlüssel",
+    "key-name": "Name des Schlüssels",
+    "no-keys-found": "Kein Schlüssel gefunden.",
+    "no-key-matching": "'{{key}}' nicht gefunden.",
+    "create-new-key": "Erstellen Sie einen neuen Schlüssel!",
+    "type": "Typ",
+    "type-required": "Typ der Entität ist erforderlich.",
+    "type-device": "Gerät",
+    "type-devices": "Geräte",
+    "list-of-devices": "{ count, plural, 1 {Ein Gerät} other {Liste von # Geräten} }",
+    "device-name-starts-with": "Geräte beginnend mit '{{prefix}}'",
+    "type-asset": "Objekt",
+    "type-assets": "Objekte",
+    "list-of-assets": "{ count, plural, 1 {Ein Objekt} other {Liste von # Objekten} }",
+    "asset-name-starts-with": "Objekte beginnend mit '{{prefix}}'",
+    "type-entity-view": "Entitätsansicht",
+    "type-entity-views": "Entitätsansichten",
+    "list-of-entity-views": "{ count, plural, 1 {Eine Entitätsansicht} other {Liste von # Entitätsansichten} }",
+    "entity-view-name-starts-with": "Entitätsansichten beginnend mit'{{prefix}}'",
+    "type-rule": "Regel",
+    "type-rules": "Regeln",
+    "list-of-rules": "{ count, plural, 1 {Eine Regel} other {Liste von # Regeln} }",
+    "rule-name-starts-with": "Regeln beginnend mit '{{prefix}}'",
+    "type-plugin": "Plugin",
+    "type-plugins": "Plugins",
+    "list-of-plugins": "{ count, plural, 1 {Ein Plugin} other {Liste von # Plugins} }",
+    "plugin-name-starts-with": "Plugins beginnend mit '{{prefix}}'",
+    "type-tenant": "Mandant",
+    "type-tenants": "Mandanten",
+    "list-of-tenants": "{ count, plural, 1 {Ein Mandant} other {Liste von # Mandanten} }",
+    "tenant-name-starts-with": "Mandanten beginnend mit '{{prefix}}'",
+    "type-customer": "Kunde",
+    "type-customers": "Kunden",
+    "list-of-customers": "{ count, plural, 1 {Ein Kunde} other {Liste von # Kunden} }",
+    "customer-name-starts-with": "Kunden beginnend mit '{{prefix}}'",
+    "type-user": "Benutzer",
+    "type-users": "Benutzer",
+    "list-of-users": "{ count, plural, 1 {Ein Benutzer} other {Liste von # Benutzern} }",
+    "user-name-starts-with": "Benutzer beginnend mit '{{prefix}}'",
+    "type-dashboard": "Dashboard",
+    "type-dashboards": "Dashboards",
+    "list-of-dashboards": "{ count, plural, 1 {Ein Dashboard} other {Liste von # Dashboards} }",
+    "dashboard-name-starts-with": "Dashboards beginnend mit '{{prefix}}'",
+    "type-alarm": "Alarm",
+    "type-alarms": "Alarme",
+    "list-of-alarms": "{ count, plural, 1 {Ein Alarm} other {Liste von # Alarmen} }",
+    "alarm-name-starts-with": "Alarme, beginnend mit '{{prefix}}'",
+    "type-rulechain": "Regelkette",
+    "type-rulechains": "Regelketten",
+    "list-of-rulechains": "{ count, plural, 1 {Eine Regelkette} other {Liste von # Regelketten} }",
+    "rulechain-name-starts-with": "Regelketten beginnend mit '{{prefix}}'",
+    "type-rulenode": "Regelknoten",
+    "type-rulenodes": "Regelknoten",
+    "list-of-rulenodes": "{ count, plural, 1 {Ein Regelknoten} other {Liste von # Regelknoten} }",
+    "rulenode-name-starts-with": "Regelknoten beginnend mit '{{prefix}}'",
+    "type-current-customer": "Aktueller Kunde",
+    "search": "Entitäten suchen",
+    "selected-entities": "{ count, plural, 1 {Entität} other {# Entitäten} } ausgewählt",
+    "entity-name": "Entitätsname",
+    "details": "Entitätsdetails",
+    "no-entities-prompt": "Keine Entitäten gefunden",
+    "no-data": "Keine Daten zum Anzeigen",
+    "columns-to-display": "Anzuzeigende Spalten"
+  },
+  "entity-view": {
+    "entity-view": "Entitätsansicht",
+    "entity-view-required": "Entitätsansicht ist erforderlich.",
+    "entity-views": "Entitätsansichten",
+    "management": "Entitätsansichten verwalten",
+    "view-entity-views": "Entitätsansichten anzeigen",
+    "entity-view-alias": "Entitätsansichtsalias",
+    "aliases": "Entitätsansichten-Aliase",
+    "no-alias-matching": "'{{alias}}' nicht gefunden.",
+    "no-aliases-found": "Keine Aliase gefunden.",
+    "no-key-matching": "'{{key}}' nicht gefunden.",
+    "no-keys-found": "Keine Schlüssel gefunden.",
+    "create-new-alias": "Neuen Alias erstellen!",
+    "create-new-key": "Neuen Schlüssel erstellen!",
+    "duplicate-alias-error": "Doppelter Alias gefunden '{{alias}}'.<br>Aliase der Entitätsansicht müssen innerhalb des Dashboards eindeutig sein.",
+    "configure-alias": "Alias '{{alias}}' konfigurieren",
+    "no-entity-views-matching": "Keine passenden Entitätsansichten für '{{entity}}' gefunden.",
+    "alias": "Alias",
+    "alias-required": "Alias der Entitätsansicht erforderlich.",
+    "remove-alias": "Alias der Entitätsansicht entfernen",
+    "add-alias": "Alias für die Entitätsansicht hinzufügen",
+    "name-starts-with": "Entitätsansichtsname beginnend mit",
+    "entity-view-list": "Liste der Entitätsansichten",
+    "use-entity-view-name-filter": "Filter anwenden",
+    "entity-view-list-empty": "Keine der Entitätsansichten ausgewählt.",
+    "entity-view-name-filter-required": "Filterung nach Entitätsansichtenname erforderlich.",
+    "entity-view-name-filter-no-entity-view-matched": "Keine Entitätsansichten beginnend mit '{{entityView}}' wurden gefunden.",
+    "add": "Entitätsansicht hinzufügen",
+    "assign-to-customer": "Einem Kunden zuordnen",
+    "assign-entity-view-to-customer": "Entitätsansichten dem Kunden zuordnen",
+    "assign-entity-view-to-customer-text": "Bitte wählen Sie die Entitätsansichten aus, die dem Kunden zugeordnet werden sollen",
+    "no-entity-views-text": "Keine Entitätsansichten gefunden",
+    "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Entitätsansichten zugeordnet werden sollen",
+    "entity-view-details": "Details der Entitätsansicht",
+    "add-entity-view-text": "Neue Entitätsansicht hinzufügen",
+    "delete": "Entitätsansicht löschen",
+    "assign-entity-views": "Entitätsansicht zuordnen",
+    "assign-entity-views-text": "Dem Kunden { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} } zuordnen",
+    "delete-entity-views": "Entitätsansichten löschen",
+    "unassign-from-customer": "Zuordnung zum Kunden aufheben",
+    "unassign-entity-views": "Zuordnung der Entitätsansichten aufheben",
+    "unassign-entity-views-action-title": "Die Zuordnung { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} } zum Kunden aufheben",
+    "assign-new-entity-view": "Neue Entitätsansicht zuordnen",
+    "delete-entity-view-title": "Möchten Sie die Entitätsansicht wirklich löschen '{{entityViewName}}'?",
+    "delete-entity-view-text": "Seien Sie vorsichtig, nach der Bestätigung werden die Entitätsansicht und alle zugehörigen Daten nicht wiederhergestellt.",
+    "delete-entity-views-title": "Sind Sie sicher, dass Sie die Entitätsansichten löschen möchten { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }?",
+    "delete-entity-views-action-title": "Löschen { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }",
+    "delete-entity-views-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Entitätsansichten entfernt und alle zugehörigen Daten werden nicht wiederhergestellt.",
+    "unassign-entity-view-title": "Möchten Sie die Zuordnung der Entitätsansicht '{{entityViewName}}' wirklich aufheben?",
+    "unassign-entity-view-text": "Nach der Bestätigung wird die Zuordnung der Entitätsansicht aufgehoben und ist für den Kunden nicht mehr zugänglich.",
+    "unassign-entity-view": "Zuordnung der Entitätsansicht aufheben",
+    "unassign-entity-views-title": "Sind Sie sicher, dass Sie die Zuordnung aufheben möchten { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichten} }?",
+    "unassign-entity-views-text": "Nach der Bestätigung werden die Zuordnungen der ausgewählten Entitätsansichten aufgehoben und sind für den Kunden nicht mehr zugänglich.",
+    "entity-view-type": "Entitätsansichtstyp",
+    "entity-view-type-required": "Entitätsansichtstyp ist erforderlich.",
+    "select-entity-view-type": "Entitätsansichtstyp auswählen",
+    "enter-entity-view-type": "Entitätsansichtstyp eingeben",
+    "any-entity-view": "Jede Entitätsansicht",
+    "no-entity-view-types-matching": "Es wurden keine passenden Entitätsansichtstypen für '{{entitySubtype}}' gefunden.",
+    "entity-view-type-list-empty": "Keine Entitätsansichtstypen ausgewählt.",
+    "entity-view-types": "Entitätsansichtstypen",
+    "name": "Name",
+    "name-required": "Name ist erforderlich.",
+    "description": "Beschreibung",
+    "events": "Ereignisse",
+    "details": "Details",
+    "copyId": "Entitätsansichts-ID kopieren",
+    "assignedToCustomer": "Dem Kunden zuordnen",
+    "unable-entity-view-device-alias-title": "Alias der Entitätsansicht kann nicht gelöscht werden",
+    "unable-entity-view-device-alias-text": "Geräte-Alias '{{entityViewAlias}}' kann nicht gelöscht werden, da es von den folgenden widget(s):<br/>{{widgetsList}} verwendet wird",
+    "select-entity-view": "Entitätsansicht auswählen",
+    "make-public": "Entitätsansicht öffentlich machen",
+    "start-date": "Start-Datum",
+    "start-ts": "Start-Zeit",
+    "end-date": "Ende-Datum",
+    "end-ts": "Ende-Zeit",
+    "date-limits": "Datumslimits",
+    "client-attributes": "Client Eigenschaften",
+    "shared-attributes": "Gemeinsame Eigenschaften",
+    "server-attributes": "Server Eigenschaften",
+    "timeseries": "Zeitreihe",
+    "client-attributes-placeholder": "Client Eigenschaften",
+    "shared-attributes-placeholder": "Gemeinsame Eigenschaften",
+    "server-attributes-placeholder": "Server Eigenschaften",
+    "timeseries-placeholder": "Zeitreihe",
+    "target-entity": "Zielentität",
+    "attributes-propagation": "Eigenschaftsübertragung",
+    "attributes-propagation-hint": "Die Entitätsansicht kopiert automatisch die angegebenen Eigenschaften der Ziel-Entität, wenn Sie diese Entitätsansicht speichern oder aktualisieren. Aus Performance-Gründen werden die Attribute der Ziel-Entität nicht bei jeder Eigenschaftsänderung in die Entitätsansicht übertragen. Sie können die automatische Weitergabe aktivieren, indem Sie den Regelknoten \"copy to view\" in Ihrer Regelkette konfigurieren und die Nachrichten \"Post attributes\" und \"Attributes updated\" mit dem neuen Regelknoten verknüpfen.",
+    "timeseries-data": "Zeitreihendaten",
+    "timeseries-data-hint": "Konfigurieren Sie die Datensatzschlüssel der Zeitreihe der Zielentität, auf die die Entitätsansicht zugreifen kann. Die Daten dieser Zeitreihe sind schreibgeschützt."
+  },
+  "event": {
+    "event-type": "Ereignistyp",
+    "type-error": "Fehler",
+    "type-lc-event": "Lebenszyklusereignis",
+    "type-stats": "Statistiken",
+    "type-debug-rule-node": "Fehlersuche",
+    "type-debug-rule-chain": "Fehlersuche",
+    "no-events-prompt": "Keine Ereignisse gefunden",
+    "error": "Fehler",
+    "alarm": "Alarm",
+    "event-time": "Ereigniszeit",
+    "server": "Server",
+    "body": "Inhalt",
+    "method": "Methode",
+    "type": "Typ",
+    "entity": "Entität",
+    "message-id": "Nachrichten-Id",
+    "message-type": "Nachrichten-Typ",
+    "data-type": "Datentyp",
+    "relation-type": "Beziehungstyp",
+    "metadata": "Meta-Daten",
+    "data": "Daten",
+    "event": "Ereignis",
+    "status": "Status",
+    "success": "Erfolg",
+    "failed": "Fehlgeschlagen",
+    "messages-processed": "Nachrichten verarbeitet",
+    "errors-occurred": "Fehler aufgetreten"
+  },
+  "extension": {
+    "extensions": "Erweiterungen",
+    "selected-extensions": "{ count, plural, 1 {Erweiterung} other {# extensions} } ausgewählt",
+    "type": "Typ",
+    "key": "Schlüssel",
+    "value": "Wert",
+    "id": "ID",
+    "extension-id": "Erweiterungs-ID",
+    "extension-type": "Erweiterungstyp",
+    "transformer-json": "JSON *",
+    "unique-id-required": "Die aktuelle Erweiterungs-ID ist bereits vorhanden.",
+    "delete": "Erweiterung löschen",
+    "add": "Erweiterung hinzufügen",
+    "edit": "Erweiterung bearbeiten",
+    "delete-extension-title": "Möchten Sie die Erweiterung '{{extensionId}}' wirklich löschen?",
+    "delete-extension-text": "Vorsicht, nach Bestätigung werden die Erweiterung und alle zugehörigen Daten nicht wiederhergestellt.",
+    "delete-extensions-title": "Möchten Sie wirklich löschen? { count, plural, 1 {1 extension} other {# extensions} }?",
+    "delete-extensions-text": "Vorsicht, nach der Bestätigung werden alle ausgewählten Erweiterungen entfernt.",
+    "converters": "Konverter",
+    "converter-id": "Konverter-ID",
+    "configuration": "Konfiguration",
+    "converter-configurations": "Konvertierte Konfigurationen",
+    "token": "Sicherheitszeichen",
+    "add-converter": "Konverter hinzufügen",
+    "add-config": "Konvertierte Konfigurationen hinzufügen",
+    "device-name-expression": "Angabe des Gerätenamens",
+    "device-type-expression": "Angabe des Gerätetyps",
+    "custom": "Regel",
+    "to-double": "Duplizieren",
+    "transformer": "Transformator",
+    "json-required": "Transformer json ist erforderlich.",
+    "json-parse": "Transformer json kann nicht analysiert werden.",
+    "attributes": "Eigenschaften",
+    "add-attribute": "Eigenschaften hinzufügen",
+    "add-map": "Mapping-Element hinzufügen",
+    "timeseries": "Zeitreihe",
+    "add-timeseries": "Zeitreihe hinzufügen",
+    "field-required": "Feld ist erforderlich",
+    "brokers": "Vermittler",
+    "add-broker": "Vermittler hinzufügen",
+    "host": "Host",
+    "port": "Port",
+    "port-range": "Der Port sollte im Bereich von 1 bis 65535 liegen.",
+    "ssl": "Ssl",
+    "credentials": "Zugangsdaten",
+    "username": "Benutzername",
+    "password": "Passwort",
+    "retry-interval": "Wiederholungsintervall in Millisekunden",
+    "anonymous": "Anonym",
+    "basic": "Basic",
+    "pem": "PEM",
+    "ca-cert": "CA-Zertifikatsdatei *",
+    "private-key": "Privatschlüsseldatei *",
+    "cert": "Zertifikatsdatei *",
+    "no-file": "Keine Datei ausgewählt.",
+    "drop-file": "Legen Sie eine Datei ab oder wählen Sie eine Datei aus um diese hochzuladen.",
+    "mapping": "Mapping",
+    "topic-filter": "Themenfilter",
+    "converter-type": "Konvertierungstyp",
+    "converter-json": "Json",
+    "json-name-expression": "Angabe des Gerätenamens json",
+    "topic-name-expression": "Themenangabe des Gerätenamens",
+    "json-type-expression": "Angabe des Gerätenamens json",
+    "topic-type-expression": "Themenangabe des Gerätetyps",
+    "attribute-key-expression": "Angabe des Eigenschaftenschlüssels",
+    "attr-json-key-expression": "Angabe des Eigenschaftenschlüssels",
+    "attr-topic-key-expression": "Themenangabe des Eigenschaftenschlüssels",
+    "request-id-expression": "ID-Angabe anfordern",
+    "request-id-json-expression": "ID-Angabe anforern json",
+    "request-id-topic-expression": "Themenangabe der ID anfordern",
+    "response-topic-expression": "Antwort Themenangabe",
+    "value-expression": "Wertangabe",
+    "topic": "Thema",
+    "timeout": "Unterbrechung in Millisekunden",
+    "converter-json-required": "Konvertierte json ist erforderlich.",
+    "converter-json-parse": "Konvertierte json konnte nicht analysiert werden.",
+    "filter-expression": "Filterangabe",
+    "connect-requests": "Abfragen verbinden",
+    "add-connect-request": "Verbindungsabfrage hinzufügen",
+    "disconnect-requests": "Abfrage trennen",
+    "add-disconnect-request": "Trennung der Abfrage hinzufügen",
+    "attribute-requests": "Abfrage der Eigenschaften",
+    "add-attribute-request": "Abfrage der Eigenschaften hinzufügen",
+    "attribute-updates": "Aktualisierungen der Eigenschaften",
+    "add-attribute-update": "Aktualisierung der Eigenschaften hinzufügen",
+    "server-side-rpc": "Serverseite RPC",
+    "add-server-side-rpc-request": "Abfrage der Serverseite RPC hinzufügen",
+    "device-name-filter": "Gerätenamefilter",
+    "attribute-filter": "Eigenschaftenfilter",
+    "method-filter": "Methodenfilter",
+    "request-topic-expression": "Themenabgabe anfordern",
+    "response-timeout": "Antwortzeit in Millisekunden",
+    "topic-expression": "Themenangabe",
+    "client-scope": "Kundenumfrage",
+    "add-device": "Gerät hinzufügen",
+    "opc-server": "Servers",
+    "opc-add-server": "Server hinzufügen",
+    "opc-add-server-prompt": "Bitte einen Server hinzufügen",
+    "opc-application-name": "Anwendungsname",
+    "opc-application-uri": "Anwendung uri",
+    "opc-scan-period-in-seconds": "Scanzeitraum in Sekunden",
+    "opc-security": "Sicherheit",
+    "opc-identity": "Identifizierung",
+    "opc-keystore": "Schlüsselspeicher",
+    "opc-type": "Typ",
+    "opc-keystore-type": "Typ",
+    "opc-keystore-location": "Standort *",
+    "opc-keystore-password": "Passwort",
+    "opc-keystore-alias": "Alias",
+    "opc-keystore-key-password": "Schlüsselpasswort",
+    "opc-device-node-pattern": "Geräteknotenmuster",
+    "opc-device-name-pattern": "Gerätenamensmuster",
+    "modbus-server": "Servers/Folgegerät",
+    "modbus-add-server": "Server/Folgegerät hinzufügen",
+    "modbus-add-server-prompt": "Bitte Server/Folgegerät hinzufügen",
+    "modbus-transport": "Transport",
+    "modbus-tcp-reconnect": "Verbindung automatisch wiederherstellen",
+    "modbus-rtu-over-tcp": "RTU über TCP",
+    "modbus-port-name": "Name des Seriellen Anschlusses",
+    "modbus-encoding": "Verschlüsselung",
+    "modbus-parity": "Übereinstimmung",
+    "modbus-baudrate": "Datenübertragungsgeschwindigkeit",
+    "modbus-databits": "Daten Bits",
+    "modbus-stopbits": "Stopp-Bits",
+    "modbus-databits-range": "Datenbits sollten im Bereich von 7 bis 8 liegen.",
+    "modbus-stopbits-range": "Stoppbits sollten im Bereich von 1 bis 2 liegen.",
+    "modbus-unit-id": "ID der Einheit",
+    "modbus-unit-id-range": "Die Einheiten-ID sollte im Bereich von 1 bis 247 liegen.",
+    "modbus-device-name": "Gerätename",
+    "modbus-poll-period": "Abfragezeitraum in Millisekunden",
+    "modbus-attributes-poll-period": "Abfrageintervall der Eigenschaften in Millisekunden",
+    "modbus-timeseries-poll-period": "Abfrageintervall der Zeitreihen in Millisekunden",
+    "modbus-poll-period-range": "Das Abfrageintervall sollte einen positiven Wert haben.",
+    "modbus-tag": "Kennzeichnung",
+    "modbus-function": "Funktion",
+    "modbus-register-address": "Registeradresse",
+    "modbus-register-address-range": "Die Registeradresse sollte im Bereich zwischen 0 und 65535 liegen.",
+    "modbus-register-bit-index": "Bitindex",
+    "modbus-register-bit-index-range": "Der Bitindex sollte im Bereich von 0 bis 15 liegen.",
+    "modbus-register-count": "Registeranzahl",
+    "modbus-register-count-range": "Die Registeranzahl sollten einen positiven Wert haben.",
+    "modbus-byte-order": "Byte-Reihenfolge",
+    "sync": {
+      "status": "Status",
+      "sync": "Synchronisiert",
+      "not-sync": "Nicht synchronisiert",
+      "last-sync-time": "Zeit der letzten Synchronisierung",
+      "not-available": "Nicht verfügbar"
+    },
+    "export-extensions-configuration": "Erweiterungskonfiguration exportieren",
+    "import-extensions-configuration": "Erweiterungskonfiguration importieren",
+    "import-extensions": "Erweiterungen importieren",
+    "import-extension": "Erweiterung importieren",
+    "export-extension": "Erweiterung exportieren",
+    "file": "Erweiterungsdatei",
+    "invalid-file-error": "Ungültige Erweiterungsdatei"
+  },
+  "fullscreen": {
+    "expand": "Auf Vollbildmodus erweitern",
+    "exit": "Vollbildmodus verlassen",
+    "toggle": "Vollbildmodus umschalten",
+    "fullscreen": "Vollbild"
+  },
+  "function": {
+    "function": "Funktion"
+  },
+  "grid": {
+    "delete-item-title": "Möchten Sie dieses Element wirklich löschen?",
+    "delete-item-text": "Vorsicht, nach Bestätigung wird das Element und alle zugehörigen Daten nicht wiederhergestellt.",
+    "delete-items-title": "Sind Sie sicher, dass Sie löschen möchten { count, plural, 1 {Symbol} other {Symbole} }?",
+    "delete-items-action-title": "Löschen { count, plural, 1 {Symbol} other {# Symbole} }",
+    "delete-items-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Elemente entfernt und alle zugehörigen Daten nicht wiederhergestellt.",
+    "add-item-text": "Neues Element hinzufügen",
+    "no-items-text": "Keine Elemente gefunden",
+    "item-details": "Elementdetails",
+    "delete-item": "Element löschen",
+    "delete-items": "Elemente löschen",
+    "scroll-to-top": "zum Seitenanfang"
+  },
+  "help": {
+    "goto-help-page": "Gehen Sie zur Hilfeseite"
+  },
+  "home": {
+    "home": "Startseite",
+    "profile": "Profil",
+    "logout": "Abmelden",
+    "menu": "Menü",
+    "avatar": "Benutzerbild",
+    "open-user-menu": "Benutzermenü öffnen"
+  },
+  "import": {
+    "no-file": "Keine Datei ausgewählt",
+    "drop-file": "Legen Sie eine JSON-Datei ab oder wählen Sie eine Datei zum hochladen aus."
+  },
+  "item": {
+    "selected": "Ausgewählt"
+  },
+  "js-func": {
+    "no-return-error": "Funktion muss einen Wert zurückgeben!",
+    "return-type-mismatch": "Funktion muss einen Wert vom Typ '{{type}}' zurückgeben!",
+    "tidy": "Aufräumen"
+  },
+  "key-val": {
+    "key": "Schlüssel",
+    "value": "Wert",
+    "remove-entry": "Eintrag entfernen",
+    "add-entry": "Eintag hinzufügen",
+    "no-data": "Keine Einträge"
+  },
+  "layout": {
+    "layout": "Layout",
+    "manage": "Layouts verwalten",
+    "settings": "Layout-Einstellungen",
+    "color": "Farbe",
+    "main": "Hauptbereich",
+    "right": "Recht",
+    "select": "Wählen Sie das Ziellayout aus"
+  },
+  "legend": {
+    "position": "Legendenposition",
+    "show-max": "Maximalwert anzeigen",
+    "show-min": "Minimalwert anzeigen",
+    "show-avg": "Durchschnittswert anzeigen",
+    "show-total": "Gesamtwert anzeigen",
+    "settings": "Legendeneinstellungen",
+    "min": "min.",
+    "max": "max.",
+    "avg": "mittelw.",
+    "total": "Gesamt"
+  },
+  "login": {
+    "login": "Login",
+    "request-password-reset": "Passwortzurücksetzung anfordern",
+    "reset-password": "Passwort zurücksetzen",
+    "create-password": "Passwort erstellen",
+    "passwords-mismatch-error": "Eingegebene Passwörter müssen identisch sein!",
+    "password-again": "Passwort wiederholen",
+    "sign-in": "Bitte anmelden",
+    "username": "Benutzername (E-Mail)",
+    "remember-me": "Login speichern",
+    "forgot-password": "Passwort vergessen?",
+    "password-reset": "Passwort zurücksetzen",
+    "new-password": "Neues Passwort",
+    "new-password-again": "Neues Passwort wiederholen",
+    "password-link-sent-message": "Der Link zum Zurücksetzen des Passworts wurde erfolgreich versendet!",
+    "email": "E-Mail"
+  },
+  "position": {
+    "top": "Oben",
+    "bottom": "Unten",
+    "left": "Links",
+    "right": "Rechts"
+  },
+  "profile": {
+    "profile": "Profil",
+    "change-password": "Passwort ändern",
+    "current-password": "Aktuelles Passwort"
+  },
+  "relation": {
+    "relations": "Beziehungen",
+    "direction": "Richtung",
+    "search-direction": {
+      "FROM": "Von",
+      "TO": "Zu"
+    },
+    "direction-type": {
+      "FROM": "von",
+      "TO": "zu"
+    },
+    "from-relations": "Ausgehende Verbindungen",
+    "to-relations": "Eingehende Verbindungen",
+    "selected-relations": "{ count, plural, 1 {1 Beziehung} other {# Beziehungen} } ausgewählt",
+    "type": "Typ",
+    "to-entity-type": "Zum Entitätstyp",
+    "to-entity-name": "Zum Entitätsnamen",
+    "from-entity-type": "Vom Entitätstyp",
+    "from-entity-name": "Vom Entitätsnamen",
+    "to-entity": "Zur Entität",
+    "from-entity": "Von Entität",
+    "delete": "Beziehung löschen",
+    "relation-type": "Art der Beziehung",
+    "relation-type-required": "Art der Beziehung erforderlich.",
+    "any-relation-type": "Jede Art",
+    "add": "Beziehung hinzufügen",
+    "edit": "Beziehung bearbeiten",
+    "delete-to-relation-title": "Möchten Sie die Beziehung zur Einheit'{{entityName}}' wirklich löschen?",
+    "delete-to-relation-text": "Vorsicht, nach Bestätigung ist die Entität '{{entityName}}' nicht mehr mit der aktuellen Entität verbunden.",
+    "delete-to-relations-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Beziehung} other {# Beziehungen} } wirklich löschen?",
+    "delete-to-relations-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Beziehungen entfernt und die entsprechenden Entitäten sind nicht mehr mit der aktuellen Entität verbunden.",
+    "delete-from-relation-title": "Sind Sie sicher, dass Sie die Verbindung aus der Entität '{{entityName}}' löschen möchten?",
+    "delete-from-relation-text": "Vorsicht, nach Bestätigung wird die aktuelle Entität '{{entityName}}' von der Entität unabhängig sein.",
+    "delete-from-relations-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Beziehung} other {# Beziehungen} } löschen möchten?",
+    "delete-from-relations-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Beziehungen entfernt und die aktuellen Entität wird nicht mehr mit den entsprechenden Entitäten verknüpft sein.",
+    "remove-relation-filter": "Beziehungsfilter entfernen",
+    "add-relation-filter": "Beziehungsfilter hinzufügen",
+    "any-relation": "Jede Beziehung",
+    "relation-filters": "Beziehungsfilter",
+    "additional-info": "Zusätzliche Information (JSON)",
+    "invalid-additional-info": "Json der Zusätzlichen Informationen konnte nicht gelesen werden."
+  },
+  "rulechain": {
+    "rulechain": "Regelkette",
+    "rulechains": "Regelketten",
+    "root": "Wurzel",
+    "delete": "Regelkette löschen",
+    "name": "Name",
+    "name-required": "Name ist erforderlich.",
+    "description": "Beschreibung",
+    "add": "Regelkette hinzufügen",
+    "set-root": "Regelkette zur Wurzel machen",
+    "set-root-rulechain-title": "Sind Sie sicher, dass Sie die Regelkette '{{ruleChainName}}' zur Wurzel machen möchten?",
+    "set-root-rulechain-text": "Nach der Bestätigung wird die Regelkette zur Wurzel und bearbeitet alle eingehenden Transportnachrichten.",
+    "delete-rulechain-title": "Sind Sie sicher, dass Sie die Regelkette '{{ruleChainName}}' löschen möchten?",
+    "delete-rulechain-text": "Vorsichtig, nach Bestätigung werden die Regelkette und alle zugehörigen Daten gelöscht.",
+    "delete-rulechains-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Regelkette} other {# Regelketten} } löschen möchten?",
+    "delete-rulechains-action-title": "{ count, plural, 1 {1 Regelkette} other {# Regelketten} } löschen",
+    "delete-rulechains-text": "Vorsichtig, nach Bestätigung werden alle ausgewählten Regelketten entfernt und alle zugehörigen Daten werden gelöscht.",
+    "add-rulechain-text": "Neue Regelkette hinzufügen",
+    "no-rulechains-text": "Keine Regelkette gefunden",
+    "rulechain-details": "Regelketten-Details",
+    "details": "Details",
+    "events": "Ereignisse",
+    "system": "System",
+    "import": "Regelkette importieren",
+    "export": "Regelkette exportieren",
+    "export-failed-error": "Regelkette konnte nicht exportiert werden: {{error}}",
+    "create-new-rulechain": "Neue Regelkette erstellen",
+    "rulechain-file": "Regelkettendatei",
+    "invalid-rulechain-file-error": "Regelkette konnte nicht importiert werden: Ungültige Regelkettendatenstruktur.",
+    "copyId": "Regelketten-ID kopieren",
+    "idCopiedMessage": "Regelketten-ID wurde in die Zwischenablage kopiert",
+    "select-rulechain": "Regelkette auswählen",
+    "no-rulechains-matching": "Es wurden keine passenden Regelketten für '{{entity}}' gefunden.",
+    "rulechain-required": "Regelkette ist erforderlich",
+    "management": "Regelverwaltung",
+    "debug-mode": "Modus zur Fehlersuche"
+  },
+  "rulenode": {
+    "details": "Details",
+    "events": "Ereignisse",
+    "search": "Knoten suchen",
+    "open-node-library": "Knotenbibliothek öffnen",
+    "add": "Neuen Regelknoten hinzufügen",
+    "name": "Name",
+    "name-required": "Name ist erforderlich.",
+    "type": "Typ",
+    "description": "Beschreibung",
+    "delete": "Regelknoten löschen",
+    "select-all-objects": "Alle Knoten und Verbindungen auswählen",
+    "deselect-all-objects": "Auswahl aller Knoten und Verbindungen aufheben",
+    "delete-selected-objects": "Ausgewählte Knoten und Verbindungen löschen",
+    "delete-selected": "Auswahl löschen",
+    "select-all": "Alle auswählen",
+    "copy-selected": "Auswahl kopieren",
+    "deselect-all": "Nichts auswählen",
+    "rulenode-details": "Details der Regelknoten",
+    "debug-mode": "Modus zur Fehlersuche",
+    "configuration": "Konfiguration",
+    "link": "Verbindung",
+    "link-details": "Verbindungsdetails der Regelknoten",
+    "add-link": "Verbindung hinzufügen",
+    "link-label": "Verbindungsbeschriftung",
+    "link-label-required": "Verbindungsbeschriftung ist erforderlich.",
+    "custom-link-label": "Benutzerdefinierte Verbindungsbeschriftung",
+    "custom-link-label-required": "Benutzerdefinierte Verbindungsbeschriftung ist erforderlich.",
+    "link-labels": "Verbindungsbeschriftungen",
+    "link-labels-required": "Verbindungsbeschriftungen sind erforderlich.",
+    "no-link-labels-found": "Keine Verbindungsbeschriftungen gefunden",
+    "no-link-label-matching": "'{{label}}' nicht gefunden.",
+    "create-new-link-label": "Bitte erstellen Sie eine neue Verbindungsbeschriftung!",
+    "type-filter": "Filter",
+    "type-filter-details": "Eingehende Nachrichten mit konfigurierten Bedingungen filtern",
+    "type-enrichment": "Anreicherung",
+    "type-enrichment-details": "Fügen Sie zusätzliche Informationen zu den Nachrichtenmetadaten hinzu",
+    "type-transformation": "Transformation",
+    "type-transformation-details": "Ändern Sie die Nutzerdaten und Metadaten der Nachricht",
+    "type-action": "Aktion",
+    "type-action-details": "Besondere Aktion ausführen",
+    "type-external": "Extern",
+    "type-external-details": "Interagiert mit externem System",
+    "type-rule-chain": "Regelkette",
+    "type-rule-chain-details": "Leitet eingehende Nachrichten an die angegebene Regelkette weiter",
+    "type-input": "Input",
+    "type-input-details": "Logische Eingabe der Regelkette, leitet eingehende Nachrichten an die nächste zugehörige Regelkette weiter",
+    "type-unknown": "Unbekannt",
+    "type-unknown-details": "Nicht aufgelöster Regelknoten",
+    "directive-is-not-loaded": "Definierte Konfigurationsanweisung '{{directiveName}}' ist nicht verfügbar.",
+    "ui-resources-load-error": "Fehler beim Laden der Konfigurations-UI-Ressourcen.",
+    "invalid-target-rulechain": "Zielregelkette kann nicht aufgelöst werden!",
+    "test-script-function": "Skriptfunktion testen",
+    "message": "Nachricht",
+    "message-type": "Nachrichtentyp",
+    "select-message-type": "Nachrichtentyp auswählen",
+    "message-type-required": "Nachrichtentyp ist erforderlich",
+    "metadata": "Metadaten",
+    "metadata-required": "Metadateneinträge dürfen nicht leer sein.",
+    "output": "Ausgabe",
+    "test": "Test",
+    "help": "Hilfe"
+  },
+  "tenant": {
+    "tenant": "Mandant",
+    "tenants": "Mandanten",
+    "management": "Mandantenverwaltung",
+    "add": "Mandant hinzufügen",
+    "admins": "Administratoren",
+    "manage-tenant-admins": "Mandantenadministratoren verwalten",
+    "delete": "Mandant löschen",
+    "add-tenant-text": "Neuen Mandanten hinzufügen",
+    "no-tenants-text": "Keine Mandanten gefunden",
+    "tenant-details": "Mandantendetails",
+    "delete-tenant-title": "Möchten Sie den Mandanten '{{tenantTitle}}' wirklich löschen?",
+    "delete-tenant-text": "Vorsicht, nach Bestätigung werden der Mandant und alle zugehörigen Daten gelöscht.",
+    "delete-tenants-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Mandant} other {# Mandanten} } löschen möchten?",
+    "delete-tenants-action-title": "{ count, plural, 1 {1 Mandant} other {# Mandanten} } löschen",
+    "delete-tenants-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Mandanten entfernt und alle zugehörigen Daten werden gelöscht.",
+    "title": "Titel",
+    "title-required": "Titel ist erforderlich.",
+    "description": "Beschreibung",
+    "details": "Details",
+    "events": "Ereignisse",
+    "copyId": "Mandanten-ID kopieren",
+    "idCopiedMessage": "Mandanten-ID wurde in die Zwischenablage kopiert",
+    "select-tenant": "Mandant auswählen",
+    "no-tenants-matching": "Es wurden keine passenden Mandanten für '{{entity}}' gefunden.",
+    "tenant-required": "Mandant ist erforderlich"
+  },
+  "timeinterval": {
+    "seconds-interval": "{ seconds, plural, 1 {1 Sekunde} other {# Sekunden} }",
+    "minutes-interval": "{ minutes, plural, 1 {1 Minute} other {# Minuten} }",
+    "hours-interval": "{ hours, plural, 1 {1 Stunde} other {# Stunden} }",
+    "days-interval": "{ days, plural, 1 {1 Tag} other {# Tage} }",
+    "days": "Tage",
+    "hours": "Stunden",
+    "minutes": "Minuten",
+    "seconds": "Sekunden",
+    "advanced": "Erweitert"
+  },
+  "timewindow": {
+    "days": "{ days, plural, 1 { Tag } other {# Tage } }",
+    "hours": "{ hours, plural, 0 { Stunde } 1 {1 Stunde } other {# Stunden } }",
+    "minutes": "{ minutes, plural, 0 { Minute } 1 {1 Minute } other {# Minuten } }",
+    "seconds": "{ seconds, plural, 0 { Sekunde } 1 {1 Sekunde } other {# Sekunden } }",
+    "realtime": "Echtzeit",
+    "history": "Historie",
+    "last-prefix": "letzte",
+    "period": "von {{ startTime }} bis {{ endTime }}",
+    "edit": "Zeitfenster bearbeiten",
+    "date-range": "Datumsbereich",
+    "last": "Letzte",
+    "time-period": "Zeitfenster"
+  },
+  "user": {
+    "user": "User",
+    "users": "Users",
+    "customer-users": "Kunden Users",
+    "tenant-admins": "Mandanten-Administratoren",
+    "sys-admin": "System-Administrator",
+    "tenant-admin": "Mandanten-Administrator",
+    "customer": "Kunde",
+    "anonymous": "Anonym",
+    "add": "Benutzer hinzufügen",
+    "delete": "Benutzer löschen",
+    "add-user-text": "Neuen Benutzer hinzufügen",
+    "no-users-text": "Keine Benutzer gefunden",
+    "user-details": "Benutzer-Details",
+    "delete-user-title": "Möchten Sie den Benutzer '{{userEmail}}' wirklich löschen?",
+    "delete-user-text": "Vorsicht, nach Bestätigung werden der Benutzer und alle zugehörigen Daten gelöscht.",
+    "delete-users-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Benutzer} other {# Benutzer} } löschen möchten??",
+    "delete-users-action-title": "{ count, plural, 1 {1 Benutzer} other {# Benutzer} } löschen",
+    "delete-users-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Benutzer entfernt und alle zugehörigen Daten werden gelöscht.",
+    "activation-email-sent-message": "Aktivierungs E-Mail wurde erfolgreich gesendet!",
+    "resend-activation": "Aktivierung erneut senden",
+    "email": "E-Mail",
+    "email-required": "E-Mail ist erforderlich.",
+    "invalid-email-format": "Ungültiges E-Mail Format.",
+    "first-name": "Vorname",
+    "last-name": "Nachname",
+    "description": "Beschreibung",
+    "default-dashboard": "Standard-Dashboard",
+    "always-fullscreen": "Immer Vollbild",
+    "select-user": "Benutzer auswählen",
+    "no-users-matching": "Keine passenden Benutzer für '{{entity}}' gefunden.",
+    "user-required": "Benutzer ist erforderlich",
+    "activation-method": "Aktivierungsmethode",
+    "display-activation-link": "Aktivierungslink anzeigen",
+    "send-activation-mail": "Aktivierungs E-Mail senden",
+    "activation-link": "Link zur Benutzer-Aktivierung",
+    "activation-link-text": "Um den Benutzer zu aktivieren, verwenden Sie bitte folgenden <a href='{{activationLink}}' target='_blank'>Aktivierungslink</a>:",
+    "copy-activation-link": "Aktivierungslink kopieren",
+    "activation-link-copied-message": "Der Link zur Benutzer-Aktivierung wurde in die Zwischenablage kopiert ",
+    "details": "Details",
+    "login-as-tenant-admin": "Als Mandanten-Administrator anmelden",
+    "login-as-customer-user": "Als Kunden-Benutzer anmelden"
+  },
+  "value": {
+    "type": "Wertetyp",
+    "string": "Text",
+    "string-value": "Textwert",
+    "integer": "Ganzzahlig",
+    "integer-value": "Ganzzahliger Wert",
+    "invalid-integer-value": "Ungültiger ganzzahliger Wert",
+    "double": "Gleitkommazahl",
+    "double-value": "Gleitkomma Wert",
+    "boolean": "Binär",
+    "boolean-value": "Binärwert",
+    "false": "Falsch",
+    "true": "Wahr",
+    "long": "Lang"
+  },
+  "widget": {
+    "widget-library": "Widget-Bibliothek",
+    "widget-bundle": "Widget-Paket",
+    "select-widgets-bundle": "Widget-Paket auswählen",
+    "management": "Widget Verwaltung",
+    "editor": "Widget Editor",
+    "widget-type-not-found": "Problem beim Laden der Widget-Konfiguration.<br>Zugeordneter Widget-Typ wurde entfernt.",
+    "widget-type-load-error": "Widget wurde aufgrund der folgenden Fehler nicht geladen:",
+    "remove": "Widget entfernen",
+    "edit": "Widget bearbeiten",
+    "remove-widget-title": "Möchten Sie das Widget '{{widgetTitle}}' wirklich entfernen?",
+    "remove-widget-text": "Nach der Bestätigung werden das Widget und alle zugehörigen Daten nicht wiederhergestellt.",
+    "timeseries": "Zeitreihe",
+    "search-data": "Daten suchen",
+    "no-data-found": "Keine Daten gefunden",
+    "latest-values": "Neueste Werte",
+    "rpc": "Steuerungswidget",
+    "alarm": "Alarm-Widget",
+    "static": "Statisches Widget",
+    "select-widget-type": "Widget-Typ auswählen",
+    "missing-widget-title-error": "Widget-Titel muss angegeben werden!",
+    "widget-saved": "Widget gespeichert",
+    "unable-to-save-widget-error": "Das Widget kann nicht gespeichert werden! Fehlermeldung!",
+    "save": "Widget speichern",
+    "saveAs": "Widget speichern unter",
+    "save-widget-type-as": "Widget-Typ speichern unter",
+    "save-widget-type-as-text": "Bitte geben Sie den neuen Widget-Titel ein und/oder wählen Sie das Ziel-Widget-Paket aus",
+    "toggle-fullscreen": "Vollbild umschalten",
+    "run": "Widget ausführen",
+    "title": "Widget-Titel",
+    "title-required": "Widget-Titel ist erforderlich.",
+    "type": "Widget-Typ",
+    "resources": "Ressourcen",
+    "resource-url": "JavaScript/CSS URL",
+    "remove-resource": "Ressource entfernen",
+    "add-resource": "Ressource hinzufügen",
+    "html": "HTML",
+    "tidy": "Aufgeräumt",
+    "css": "CSS",
+    "settings-schema": "Einstellungsschema",
+    "datakey-settings-schema": "Datenschlüssel-Einstellungsschema",
+    "javascript": "Javascript",
+    "remove-widget-type-title": "Möchten Sie den Widget-Typ '{{widgetName}}' wirklich entfernen?",
+    "remove-widget-type-text": "Nach der Bestätigung werden der Widget-Typ und alle zugehörigen Daten nicht wiederhergestellt.",
+    "remove-widget-type": "Widget-Typ entfernen",
+    "add-widget-type": "Neuen Widget-Typ hinzufügen",
+    "widget-type-load-failed-error": "Widget-Typ konnte nicht geladen werden!",
+    "widget-template-load-failed-error": "Widget-Vorlage konnte nicht geladen werden!",
+    "add": "Widget hinzufügen",
+    "undo": "Widget-Änderungen widerrufen ",
+    "export": "Widget exportieren"
+  },
+  "widget-action": {
+    "header-button": "Widget-Header-Schaltfläche",
+    "open-dashboard-state": "Zum neuen Dashboard-Status navigieren",
+    "update-dashboard-state": "Aktuellen Dashboard-Status aktualisieren",
+    "open-dashboard": "Zu einem anderen Dashboard navigieren",
+    "custom": "Benutzerdefinierte Aktion",
+    "target-dashboard-state": "Zielstatus des Dashboards",
+    "target-dashboard-state-required": "Der Zielstatus ist erforderlich",
+    "set-entity-from-widget": "Widget-Entität festlegen",
+    "target-dashboard": "Ziel-Dashboard",
+    "open-right-layout": "Das rechte Dashboard-Layout öffnen (mobile Ansicht)"
+  },
+  "widgets-bundle": {
+    "current": "Aktuelles Paket",
+    "widgets-bundles": "Widget-Pakete",
+    "add": "Widget-Pakete hinzufügen",
+    "delete": "Widget-Pakete löschen",
+    "title": "Titel",
+    "title-required": "Titel ist erforderlich.",
+    "add-widgets-bundle-text": "Neues Widget-Paket hinzufügen",
+    "no-widgets-bundles-text": "Keine Widget-Pakete gefunden",
+    "empty": "Widget-Paket ist leer ",
+    "details": "Details",
+    "widgets-bundle-details": "Widget-Paket-Details",
+    "delete-widgets-bundle-title": "Möchten Sie das Widget-Paket '{{widgetsBundleTitle}}' wirklich löschen? ",
+    "delete-widgets-bundle-text": "Seien Sie vorsichtig, nach der Bestätigung werden das Widget-Paket und alle zugehörigen Daten gelöscht.",
+    "delete-widgets-bundles-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Widget-Paket} other {# Widget-Pakete} } löschen möchten?",
+    "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 Widgets-Paket} other {# Widget-Pakete} } löschen",
+    "delete-widgets-bundles-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Widget-Pakete entfernt und alle zugehörigen Daten werden gelöscht.",
+    "no-widgets-bundles-matching": "Keine passenden Widget-Pakete '{{widgetsBundle}}' gefunden.",
+    "widgets-bundle-required": "Widget-Paket ist erforderlich.",
+    "system": "System",
+    "import": "Widget-Paket importieren",
+    "export": "Widget-Paket exportieren",
+    "export-failed-error": "Widget-Paket kann nicht exportiert werden: {{error}}",
+    "create-new-widgets-bundle": "Neues Widget-Paket erstellen",
+    "widgets-bundle-file": "Widget-Paket-Datei",
+    "invalid-widgets-bundle-file-error": "Widget-Paket kann nicht importiert werden: Ungültige Widget-Paket-Datenstruktur."
+  },
+  "widget-config": {
+    "data": "Daten",
+    "settings": "Einstellungen",
+    "advanced": "Erweitert ",
+    "title": "Titel",
+    "general-settings": "Allgemeine Einstellungen",
+    "display-title": "Titel anzeigen",
+    "drop-shadow": "Schlagschatten",
+    "enable-fullscreen": "Vollbild aktivieren",
+    "background-color": "Hintergrundfarbe",
+    "text-color": "Textfarbe",
+    "padding": "Pufferung",
+    "margin": "Rand",
+    "widget-style": "Widget-Stil",
+    "title-style": "Titel-Stil",
+    "mobile-mode-settings": "Einstellungen für den mobilen Modus",
+    "order": "Reihenfolge",
+    "height": "Größe",
+    "units": "Spezielles Symbol, das neben dem Wert angezeigt wird",
+    "decimals": "Anzahl der Stellen nach dem Fließkomma",
+    "timewindow": "Zeitfenster",
+    "use-dashboard-timewindow": "Dashboard-Zeitfenster verwenden",
+    "display-legend": "Legende anzeigen",
+    "datasources": "Datenquellen",
+    "maximum-datasources": "Maximal { count, plural, 1 {1 Datenquelle ist erlaubt} other {# Datenquellen sind erlaubt} }.",
+    "datasource-type": "Typ",
+    "datasource-parameters": "Parameter",
+    "remove-datasource": "Datenquelle entfernen",
+    "add-datasource": "Datenquelle hinzufügen ",
+    "target-device": "Zielgerät",
+    "alarm-source": "Alarmquelle",
+    "actions": "Aktionen",
+    "action": "Aktion",
+    "add-action": "Aktion hinzufügen",
+    "search-actions": "Aktion suchen",
+    "action-source": "Aktionsquelle",
+    "action-source-required": "Aktionsquelle ist erforderlich.",
+    "action-name": "Name",
+    "action-name-required": "Aktionsname ist erforderlich.",
+    "action-name-not-unique": "Eine andere Aktion mit demselben Namen ist bereits vorhanden.<br/> Der Aktionsname sollte innerhalb derselben Aktionsquelle eindeutig sein.",
+    "action-icon": "Symbol ",
+    "action-type": "Art",
+    "action-type-required": "Aktionsart ist erforderlich.",
+    "edit-action": "Aktion bearbeiten",
+    "delete-action": "Aktion löschen",
+    "delete-action-title": "Widget-Aktion löschen",
+    "delete-action-text": "Möchten Sie die Widget-Aktion mit Namen '{{actionName}}' wirklich löschen?"
+  },
+  "widget-type": {
+    "import": "Widget-Typ importieren",
+    "export": "Widget-Typ exportieren",
+    "export-failed-error": "Widget-Typ kann nicht exportiert werden: {{error}}",
+    "create-new-widget-type": "Neuen Widget-Typ erstellen",
+    "widget-type-file": "Widget-Typdatei",
+    "invalid-widget-type-file-error": "Widget-Typ kann nicht importiert werden: Ungültige Datenstruktur des Widget-Typs."
+  },
+  "icon": {
+    "icon": "Symbol",
+    "select-icon": "Symbol auswählen",
+    "material-icons": "Material-Symbole",
+    "show-all": "Alle Symbole anzeigen"
+  },
+  "custom": {
+    "widget-action": {
+      "action-cell-button": "Aktionszellenschaltfläche",
+      "row-click": "Klick auf Zeile",
+      "marker-click": "Klick auf Marker",
+      "tooltip-tag-action": "Tooltip-Tag-Aktion"
+    }
+  },
+  "language": {
+    "language": "Sprache",
+    "locales": {
+      "de_DE": "Deutsch",
+      "fr_FR": "Französisch",
+      "zh_CN": "Chinesisch",
+      "en_US": "Englisch",
+      "it_IT": "Italienisch",
+      "ko_KR": "Koreanisch",
+      "ru_RU": "Russisch",
+      "es_ES": "Spanisch",
+      "ja_JA": "Japanisch",
+      "tr_TR": "Türkisch"
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 30e1eb1..4f12ee0 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/ui/src/app/locale/locale.constant-en_US.json
@@ -1008,6 +1008,8 @@
         "modbus-add-server": "Add server/slave",
         "modbus-add-server-prompt": "Please add server/slave",
         "modbus-transport": "Transport",
+        "modbus-tcp-reconnect": "Automatically reconnect",
+        "modbus-rtu-over-tcp": "RTU over TCP",
         "modbus-port-name": "Serial port name",
         "modbus-encoding": "Encoding",
         "modbus-parity": "Parity",
@@ -1032,7 +1034,6 @@
         "modbus-register-count": "Register count",
         "modbus-register-count-range": "Register count should be a positive value.",
         "modbus-byte-order": "Byte order",
-
         "sync": {
             "status": "Status",
             "sync": "Sync",
@@ -1040,7 +1041,6 @@
             "last-sync-time": "Last sync time",
             "not-available": "Not available"
         },
-
         "export-extensions-configuration": "Export extensions configuration",
         "import-extensions-configuration": "Import extensions configuration",
         "import-extensions": "Import extensions",
@@ -1566,7 +1566,8 @@
     "language": {
         "language": "Language",
         "locales": {
-			"fr_FR": "French",
+            "de_DE": "German",
+            "fr_FR": "French",
             "zh_CN": "Chinese",
             "en_US": "English",
             "it_IT": "Italian",
@@ -1577,4 +1578,4 @@
             "tr_TR": "Turkish"
         }
     }
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json
index 7b02141..8190157 100644
--- a/ui/src/app/locale/locale.constant-es_ES.json
+++ b/ui/src/app/locale/locale.constant-es_ES.json
@@ -2,9 +2,9 @@
     "access": {
         "unauthorized": "No autorizado",
         "unauthorized-access": "Acceso no autorizado",
-        "unauthorized-access-text": "Debe registrarse para tener acceso a este recurso!",
+        "unauthorized-access-text": "¡Debe registrarse para tener acceso a este recurso!",
         "access-forbidden": "Acceso Prohibido",
-        "access-forbidden-text": "No tiene derechos para acceder a esta ubicación!<br/>Intente registrarse con otro usuario si aún desea acceder a esta ubicación.",
+        "access-forbidden-text": "No tiene permisos para acceder a esta ubicación!<br/>Intente registrarse con otro usuario si aún desea acceder a esta ubicación.",
         "refresh-token-expired": "La sesión ha expirado",
         "refresh-token-failed": "No se puede actualizar la sesión"
     },
@@ -14,7 +14,7 @@
         "save": "Guardar",
         "saveAs": "Guardar como",
         "cancel": "Cancelar",
-        "ok": "OK",
+        "ok": "Aceptar",
         "delete": "Eliminar",
         "add": "Agregar",
         "yes": "Si",
@@ -30,17 +30,17 @@
         "apply": "Aplicar",
         "apply-changes": "Aplicar cambios",
         "edit-mode": "Modo edición",
-        "enter-edit-mode": "Ingresar modo edición",
+        "enter-edit-mode": "Entrar en modo edición",
         "decline-changes": "Descartar cambios",
         "close": "Cerrar",
         "back": "Atras",
         "run": "Ejecutar",
-        "sign-in": "Registrarse!",
+        "sign-in": "¡Registrarse!",
         "edit": "Editar",
         "view": "Ver",
         "create": "Crear",
         "drag": "Arrastrar",
-        "refresh": "Refrecar",
+        "refresh": "Actualizar",
         "undo": "Deshacer",
         "copy": "Copiar",
         "paste": "Pegar",
@@ -48,7 +48,7 @@
         "paste-reference": "Pegar referencia",
         "import": "Importar",
         "export": "Exportar",
-        "share-via": "Share via {{provider}}"
+        "share-via": "Compartir via {{provider}}"
     },
     "aggregation": {
         "aggregation": "Agregación",
@@ -68,11 +68,11 @@
         "outgoing-mail": "Servidor de correo",
         "outgoing-mail-settings": "Configuración del servidor de correo de salida",
         "system-settings": "Configuración del sistema",
-        "test-mail-sent": "Correo de prueba fue enviado exitosamente!",
+        "test-mail-sent": "¡El correo de prueba fue enviado correctamente!",
         "base-url": "URL base",
         "base-url-required": "URL base es requerida.",
         "mail-from": "Correo desde",
-        "mail-from-required": "Correo Desde es requerido.",
+        "mail-from-required": "Correo desde es requerido.",
         "smtp-protocol": "Protocolo SMTP",
         "smtp-host": "Host SMTP",
         "smtp-host-required": "Host SMTP es requerido.",
@@ -93,11 +93,11 @@
         "alarm-required": "Alarma es requerida",
         "alarm-status": "Estado de la alarma",
         "search-status": {
-            "ANY": "Alguna",
-            "ACTIVE": "Activa",
-            "CLEARED": "Borrada",
-            "ACK": "Reconocida",
-            "UNACK": "Ignorada"
+            "ANY": "Todas",
+            "ACTIVE": "Activas",
+            "CLEARED": "Borradas",
+            "ACK": "Reconocidas",
+            "UNACK": "Ignoradas"
         },
         "display-status": {
             "ACTIVE_UNACK": "Activa ignorada",
@@ -105,10 +105,10 @@
             "CLEARED_UNACK": "Borrada ignorada",
             "CLEARED_ACK": "Borrada reconocida"
         },
-        "no-alarms-prompt": "Alarmas no encontradas",
+        "no-alarms-prompt": "No se encontraron alarmas",
         "created-time": "Tiempo de creación",
         "type": "Tipo",
-        "severity": "Severidad",
+        "severity": "Criticidad",
         "originator": "Origen",
         "originator-type": "Tipo de origen",
         "details": "Detalles",
@@ -119,27 +119,27 @@
         "ack-time": "Tiempo de reconocimiento",
         "clear-time": "Tiempo de borrado",
         "severity-critical": "Crítica",
-        "severity-major": "Mayor",
-        "severity-minor": "Menor",
+        "severity-major": "Alta",
+        "severity-minor": "Baja",
         "severity-warning": "Alerta",
         "severity-indeterminate": "Indeterminada",
         "acknowledge": "Reconocer",
         "clear": "Borrar",
         "search": "buscar alarmas",
-        "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } seleccionadas",
+        "selected-alarms": "{ count, plural, 1 {1 alarma} other {# alarmas} } seleccionadas",
         "no-data": "No hay datos para mostrar",
         "polling-interval": "Intervalo de sondeo de alarmas (seg)",
         "polling-interval-required": "Intervalo de sondeo de alarmas es requerido.",
         "min-polling-interval-message": "Se permite al menos 1 segundo de intervalo de sondeo.",
-        "aknowledge-alarms-title": "Reconocer { count, plural, 1 {1 alarm} other {# alarms} }",
-        "aknowledge-alarms-text": "¿Está seguro de que desea reconocer { count, plural, 1 {1 alarm} other {# alarms} }?",
-        "aknowledge-alarm-title": "Acknowledge Alarm",
-        "aknowledge-alarm-text": "Are you sure you want to acknowledge Alarm?",
-        "clear-alarms-title": "Borrar { count, plural, 1 {1 alarm} other {# alarms} }",
-        "clear-alarms-text": "¿Está seguro de que desea borrar { count, plural, 1 {1 alarm} other {# alarms} }?",
-        "clear-alarm-title": "Clear Alarm",
-        "clear-alarm-text": "Are you sure you want to clear Alarm?",
-        "alarm-status-filter": "Alarm Status Filter"
+        "aknowledge-alarms-title": "Reconocer { count, plural, 1 {1 alarma} other {# alarmas} }",
+        "aknowledge-alarms-text": "¿Está seguro de que desea reconocer { count, plural, 1 {1 alarma} other {# alarmas} }?",
+        "aknowledge-alarm-title": "Reconocer alarma",
+        "aknowledge-alarm-text": "¿Está seguro que quirere reconocer la alarma?",
+        "clear-alarms-title": "Quitar { count, plural, 1 {1 alarma} other {# alarmas} }",
+        "clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas}?",
+        "clear-alarm-title": "Quitar alarma",
+        "clear-alarm-text": "¿Está seguro que quiere quitar la alarma?",
+        "alarm-status-filter": "Filtro de estado de alarma"
     },
     "alias": {
         "add": "Agregar alias",
@@ -151,24 +151,29 @@
         "filter-type-entity-list": "Lista de entidades",
         "filter-type-entity-name": "Nombre de entidad",
         "filter-type-state-entity": "Entidad del panel de estados",
-        "filter-type-state-entity-description": "Entidad tomada desde los parámetro del panel de estados",
+        "filter-type-state-entity-description": "Entidad tomada desde los parámetros del panel de estados",
         "filter-type-asset-type": "Tipo de activo",
         "filter-type-asset-type-description": "Activos de tipo '{{assetType}}'",
-        "filter-type-asset-type-and-name-description": "Activos de tipo '{{assetType}}' y con nombre comenzando con '{{prefix}}'",
+        "filter-type-asset-type-and-name-description": "Activos del tipo '{{assetType}}' y su nombre empieza con '{{prefix}}'",
         "filter-type-device-type": "Tipo de dispositivo",
         "filter-type-device-type-description": "Dispositivos de tipo '{{deviceType}}'",
-        "filter-type-device-type-and-name-description": "Dispositivos de tipo '{{deviceType}}' y con nombre comenzando con '{{prefix}}'",
+        "filter-type-device-type-and-name-description": "Dispositivos del tipo '{{deviceType}}' y  su nombre empieza con '{{prefix}}'",
+        "filter-type-entity-view-type": "Tipo de vista de entidad",
+        "filter-type-entity-view-type-description": "Vista de entidad del tipo '{{entityView}}'",
+        "filter-type-entity-view-type-and-name-description": "Las vista de entidad del tipo '{{entityView}}' y cuyo nombre comienza con '{{prefix}}'",
         "filter-type-relations-query": "Consulta de relaciones",
         "filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}",
         "filter-type-asset-search-query": "Consultar búsqueda de activos",
         "filter-type-asset-search-query-description": "Activos con tipos {{assetTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}",
         "filter-type-device-search-query": "Consultar búqueda de dispositivos",
         "filter-type-device-search-query-description": "Dispositivos con tipos {{deviceTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}",
+        "filter-type-entity-view-search-query": "Consultar vista de entidad",
+        "filter-type-entity-view-search-query-description": "Las vista de entidad de tipo {{entityViewTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}",
         "entity-filter": "Filtro de entidad",
         "resolve-multiple": "Resolver como entidades múltiples",
         "filter-type": "Tipo de filtro",
         "filter-type-required": "Tipo de filtro es requerido.",
-        "entity-filter-no-entity-matched": "Entidades que coincidan con el filtro especificado no fueron encontradas.",
+        "entity-filter-no-entity-matched": "No se encontraron entidades que coincidan con el filtro especificado.",
         "no-entity-filter-specified": "No se especificó el filtro de entidad",
         "root-state-entity": "Utilizar la entidad del panel de estados como raíz",
         "root-entity": "Entidad raíz",
@@ -185,13 +190,13 @@
         "asset": "Activo",
         "assets": "Activos",
         "management": "Gestión de activos",
-        "view-assets": "Ver Activos",
-        "add": "Agregar Activo",
+        "view-assets": "Ver activos",
+        "add": "Agregar activo",
         "assign-to-customer": "Asignar al cliente",
-        "assign-asset-to-customer": "Asignar Activo(s) Al Cliente",
-        "assign-asset-to-customer-text": "Por favor seleccionar los activos para asignar al cliente",
-        "no-assets-text": "Activos no encontrados",
-        "assign-to-customer-text": "Por favor seleccionar el cliente para asignar el(los) activo(s)",
+        "assign-asset-to-customer": "Asignar activo(s) al cliente",
+        "assign-asset-to-customer-text": "Por favor, seleccione los activos para asignar al cliente",
+        "no-assets-text": "No se encontraron activos",
+        "assign-to-customer-text": "Por favor, seleccione el cliente para asignar el(los) activo(s)",
         "public": "Público",
         "assignedToCustomer": "Asignado al cliente",
         "make-public": "Hacer público el activo",
@@ -202,9 +207,9 @@
         "asset-type": "Tipo de activo",
         "asset-type-required": "El tipo de activo es requerido.",
         "select-asset-type": "Seleccionar tipo de activo",
-        "enter-asset-type": "Ingresar tipo de activo",
+        "enter-asset-type": "Introduzca el tipo de activo",
         "any-asset": "Algún activo",
-        "no-asset-types-matching": "Tipos de activos que coincidan con '{{entitySubtype}}' no fueron encontrados.",
+        "no-asset-types-matching": "No se encontraron tipos de activos que coincidan con '{{entitySubtype}}'.",
         "asset-type-list-empty": "No se seleccionaron tipos de activos.",
         "asset-types": "Tipos de activos",
         "name": "Nombre",
@@ -217,16 +222,16 @@
         "add-asset-text": "Agregar nuevos activos",
         "asset-details": "Detalles del activo",
         "assign-assets": "Asignar activos",
-        "assign-assets-text": "Asignar { count, plural, 1 {1 asset} other {# assets} } al cliente",
+        "assign-assets-text": "Asignar { count, plural, 1 {1 activo} other {# activos} } al cliente",
         "delete-assets": "Eliminar activos",
         "unassign-assets": "Anular asignación de activos",
-        "unassign-assets-action-title": "Anular asignación { count, plural, 1 {1 asset} other {# assets} } del cliente",
+        "unassign-assets-action-title": "Anular asignación { count, plural, 1 {1 activo} other {# activos} } del cliente",
         "assign-new-asset": "Asignar nuevo activo",
         "delete-asset-title": "¿Está seguro de que desea eliminar el activo '{{assetName}}'?",
-        "delete-asset-text": "Tener cuidado, después de la confirmación, el activo y todos los datos relacionados se volverán irrecuperables.",
-        "delete-assets-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 asset} other {# assets} }?",
-        "delete-assets-action-title": "Eliminar { count, plural, 1 {1 asset} other {# assets} }",
-        "delete-assets-text": "Tener cuidado, después de la confirmación se eliminarán todos los activos seleccionados y todos los datos relacionados se volverán irrecuperables.",
+        "delete-asset-text": "¡Cuidado! Después de la confirmación, el activo y todos los datos relacionados serán irrecuperables.",
+        "delete-assets-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 activos} other {# activos} }?",
+        "delete-assets-action-title": "Eliminar { count, plural, 1 {1 activo} other {# activos} }",
+        "delete-assets-text": "¡Cuidado! Después de la confirmación se eliminarán todos los activos seleccionados y todos los datos relacionados serán irrecuperables.",
         "make-public-asset-title": "¿Está seguro de que desea que el activo '{{assetName}}' sea público?",
         "make-public-asset-text": "Después de la confirmación, el activo y todos sus datos se harán públicos y accesibles por otros.",
         "make-private-asset-title": "¿Está seguro de que desea que el activo '{{assetName}}' sea privado?",
@@ -234,12 +239,12 @@
         "unassign-asset-title": "¿Está seguro de que desea anular asignación del activo '{{assetName}}'?",
         "unassign-asset-text": "Después de la confirmación, se anulará asignación del activo y no será accesible por el cliente.",
         "unassign-asset": "Anular asignación activo",
-        "unassign-assets-title": "¿Está seguro de que desea anular asignación { count, plural, 1 {1 asset} other {# assets} }?",
+        "unassign-assets-title": "¿Está seguro de que desea anular asignación { count, plural, 1 {1 activo} other {# activos} }?",
         "unassign-assets-text": "Después de la confirmación, se anulará asignación de todos los activos seleccionados y no serán accesibles por el cliente",
         "copyId": "Copiar ID del activo",
         "idCopiedMessage": "ID del activo has sido copiada al portapapeles",
         "select-asset": "Seleccionar activo",
-        "no-assets-matching": "Activos que coincidan con '{{entity}}' no fueron encontrados.",
+        "no-assets-matching": "No se encontraron activos que coincidan con '{{entity}}'.",
         "asset-required": "El activo es requerido",
         "name-starts-with": "El nombre del activo comienza con"
     },
@@ -257,25 +262,25 @@
         "key-required": "La clave del aributo es requerida.",
         "value": "Valor",
         "value-required": "Valor del atributo es requerido.",
-        "delete-attributes-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 attribute} other {# attributes} }?",
-        "delete-attributes-text": "Tener cuidado, después de la confirmación, se eliminarán todos los atributos seleccionados.",
+        "delete-attributes-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 atributo} other {# atributos} }?",
+        "delete-attributes-text": "¡Cuidado! Después de la confirmación, se eliminarán todos los atributos seleccionados.",
         "delete-attributes": "Eliminar atributos",
-        "enter-attribute-value": "Ingresar valor del atributo",
+        "enter-attribute-value": "Introduzca el valor del atributo",
         "show-on-widget": "Mostrar en widget",
         "widget-mode": "Modo widget",
         "next-widget": "Widget siguiente",
         "prev-widget": "Widget previo",
         "add-to-dashboard": "Agregar al panel",
         "add-widget-to-dashboard": "Agregar widget al panel",
-        "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } seleccionados",
+        "selected-attributes": "{ count, plural, 1 {1 atributo} other {# atributos} } seleccionados",
         "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } seleccionadas"
     },
     "audit-log": {
-        "audit": "Auditar",
-        "audit-logs": "Auditar Registros",
+        "audit": "Auditoría",
+        "audit-logs": "Registros de auditoría",
         "timestamp": "Marca de tiempo",
-        "entity-type": "Tipo de Entidad",
-        "entity-name": "Nombre de Entidad",
+        "entity-type": "Tipo de entidad",
+        "entity-name": "Nombre de entidad",
         "user": "Usuario",
         "type": "Tipo",
         "status": "Estado",
@@ -287,7 +292,7 @@
         "type-attributes-deleted": "Atributos eliminados",
         "type-rpc-call": "Llamada RPC",
         "type-credentials-updated": "Credenciales actualizadas",
-        "type-assigned-to-customer": "Asignado al Cliente",
+        "type-assigned-to-customer": "Asignado al cliente",
         "type-unassigned-from-customer": "Asignación anulada del cliente",
         "type-activated": "Activado",
         "type-suspended": "Suspendido",
@@ -298,18 +303,18 @@
         "type-relations-delete": "Toda relación eliminada",
         "type-alarm-ack": "Reconocida",
         "type-alarm-clear": "Borrada",
-        "status-success": "Éxito",
-        "status-failure": "Falla",
-        "audit-log-details": "Auditar detalles de regisstro",
-        "no-audit-logs-prompt": "Registros no encontrados",
+        "status-success": "Correcto",
+        "status-failure": "Erróneo",
+        "audit-log-details": "Detalle del registro de auditoría",
+        "no-audit-logs-prompt": "No se encontraron registros",
         "action-data": "Datos de acción",
-        "failure-details": "Detalles de falla",
+        "failure-details": "Detalles del error",
         "search": "Buscar registros de auditoría",
         "clear-search": "Borrar búsqueda"
     },
     "confirm-on-exit": {
         "message": "Tiene cambios sin guardar. ¿Está seguro de que desea salir de esta página?",
-        "html-message": "Tiene cambios sin guardar..<br/>¿Está seguro de que desea salir de esta página?",
+        "html-message": "Tiene cambios sin guardar.<br/>¿Está seguro de que desea salir de esta página?",
         "title": "Cambios sin guardar"
     },
     "contact": {
@@ -327,9 +332,9 @@
     "common": {
         "username": "Nombre de usuario",
         "password": "Contraseña",
-        "enter-username": "Ingresar nombre de usuario",
-        "enter-password": "Ingresar contraseña",
-        "enter-search": "Ingresar búsqueda"
+        "enter-username": "Introduzca nombre de usuario",
+        "enter-password": "Introduzca contraseña",
+        "enter-search": "Introduzca búsqueda"
     },
     "content-type": {
         "json": "Json",
@@ -340,15 +345,15 @@
         "customer": "Cliente",
         "customers": "Clientes",
         "management": "Gestión del cliente",
-        "dashboard": "Panel del Cliente",
-        "dashboards": "Paneles del Cliente",
-        "devices": "Dispositivos del Cliente",
-        "entity-views": "Customer Entity Views",
+        "dashboard": "Panel del cliente",
+        "dashboards": "Paneles del cliente",
+        "devices": "Dispositivos del cliente",
+        "entity-views": "Vistas de entidad del cliente",
         "assets": "Activos del Cliente",
-        "public-dashboards": "Paneles Públicos",
-        "public-devices": "Dispositivos Públicos",
-        "public-assets": "Activos Públicos",
-        "public-entity-views": "Public Entity Views",
+        "public-dashboards": "Paneles públicos",
+        "public-devices": "Dispositivos públicos",
+        "public-assets": "Activos públicos",
+        "public-entity-views": "Vista de entidad públicas",
         "add": "Agregar cliente",
         "delete": "Eliminar cliente",
         "manage-customer-users": "Gestionar usuarios del cliente",
@@ -359,13 +364,13 @@
         "manage-customer-assets": "Gestionar activos del cliente",
         "manage-public-assets": "Gestionar activos públicos",
         "add-customer-text": "Agregar nuevo cliente",
-        "no-customers-text": "Clientes no encontrados",
+        "no-customers-text": "No se encontraron clientes",
         "customer-details": "Detalles del cliente",
         "delete-customer-title": "¿Está seguro de que desea eliminar al cliente '{{customerTitle}}'?",
-        "delete-customer-text": "Tener cuidado, después de la confirmación, el cliente y todos los datos relacionados se volverán irrecuperables.",
-        "delete-customers-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 customer} other {# customers} }?",
-        "delete-customers-action-title": "Eliminar { count, plural, 1 {1 customer} other {# customers} }",
-        "delete-customers-text": "Tener cuidado, después de la confirmación, todos los clientes seleccionados serán eliminados y todos los datos relacionados se volverán irrecuperables.",
+        "delete-customer-text": "¡Cuidado! Después de la confirmación, el cliente y todos los datos relacionados serán irrecuperables.",
+        "delete-customers-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 cliente} other {# clientes} }?",
+        "delete-customers-action-title": "Eliminar { count, plural, 1 {1 cliente} other {# clientes} }",
+        "delete-customers-text": "¡Cuidado! Después de la confirmación, todos los clientes seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.",
         "manage-users": "Gestionar usuarios",
         "manage-assets": "Gestionar activos",
         "manage-devices": "Gestionar dispositivos",
@@ -378,7 +383,7 @@
         "copyId": "Copiar ID del cliente",
         "idCopiedMessage": "ID del cliente ha sido copiada al portapapeles",
         "select-customer": "Seleccionar cliente",
-        "no-customers-matching": "Clientes que coincidan con '{{entity}}' no fueron encontrados.",
+        "no-customers-matching": "No se encontraron clientes que coincidan con '{{entity}}'.",
         "customer-required": "El cliente es requerido",
         "select-default-customer": "Seleccionar cliente predeterminado",
         "default-customer": "Cliente predeterminado",
@@ -394,22 +399,22 @@
         "dashboard": "Panel",
         "dashboards": "Paneles",
         "management": "Gestión del panel",
-        "view-dashboards": "Ver Panel",
-        "add": "Agregar Paneles",
-        "assign-dashboard-to-customer": "Asignar Panel(es) Al Cliente",
-        "assign-dashboard-to-customer-text": "Por favor seleccionar los paneles para asignar al cliente",
-        "assign-to-customer-text": "Por favor seleccionar el cliente para asignar el(los) panel(es)",
+        "view-dashboards": "Ver panel",
+        "add": "Agregar paneles",
+        "assign-dashboard-to-customer": "Asignar panel(es) al cliente",
+        "assign-dashboard-to-customer-text": "Por favor selecciona los paneles para asignar al cliente",
+        "assign-to-customer-text": "Por favor selecciona el cliente para asignar el(los) panel(es)",
         "assign-to-customer": "Asignar al cliente",
         "unassign-from-customer": "Anular asignación del cliente",
         "make-public": "Hacer panel público",
-        "make-private": "Hcer panel privado",
+        "make-private": "Hacer panel privado",
         "manage-assigned-customers": "Gestionar clientes asignados",
         "assigned-customers": "Clientes asignados",
-        "assign-to-customers": "Asignar Panel(es) Al(Los) Cliente(s)",
+        "assign-to-customers": "Asignar panel(es) al(los) cliente(s)",
         "assign-to-customers-text": "Por favor seleccionar los clientes para asignar el(los) panel(es)",
-        "unassign-from-customers": "Anular Asignación Del(De Los) Panel(es) De Los Clientes",
-        "unassign-from-customers-text": "Por favor seeccionar los clientes oara anular asignación del(de los) panel(es)",
-        "no-dashboards-text": "Paneles no encontrados",
+        "unassign-from-customers": "Anular asignación del(de los) panel(es) de los clientes",
+        "unassign-from-customers-text": "Por favor selecciona los clientes para anular asignación del(de los) panel(es)",
+        "no-dashboards-text": "No se encontraron paneles",
         "no-widgets": "Sin widgets configurados",
         "add-widget": "Agregar nuevo widget",
         "title": "Título",
@@ -419,7 +424,7 @@
         "title-required": "El título es requerido.",
         "description": "Descripción",
         "details": "Detalles",
-        "dashboard-details": "Detalles del Panel",
+        "dashboard-details": "Detalles del panel",
         "add-dashboard-text": "Agregar nuevo panel",
         "assign-dashboards": "Asignar paneles",
         "assign-new-dashboard": "Aignar nuevo panel",
@@ -429,10 +434,10 @@
         "unassign-dashboards": "Anular asignación de paneles",
         "unassign-dashboards-action-title": "Anular asignación { count, plural, 1 {1 dashboard} other {# dashboards} } del cliente",
         "delete-dashboard-title": "¿Está seguro de que desea eliminar el panel '{{dashboardTitle}}'?",
-        "delete-dashboard-text": "Tener cuidado, después de la confirmación, el panel y todos los datos relacionados se volverán irrecuperables.",
+        "delete-dashboard-text": "¡Cuidado! Después de la confirmación, el panel y todos los datos relacionados serán irrecuperables.",
         "delete-dashboards-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 dashboard} other {# dashboards} }?",
         "delete-dashboards-action-title": "Eliminar { count, plural, 1 {1 dashboard} other {# dashboards} }",
-        "delete-dashboards-text": "Tener cuidado, después de la confirmación, todos los paneles seleccionados serán eliminados y todos los datos relacionados se volverán irrecuperables.",
+        "delete-dashboards-text": "¡Cuidado! Después de la confirmación, todos los paneles seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.",
         "unassign-dashboard-title": "¿Está seguro de que desea anular la asignación del panel '{{dashboardTitle}}'?",
         "unassign-dashboard-text": "Después de la confirmación, se anulará la asignación del panel y no será accesible por el cliente.",
         "unassign-dashboard": "Anular asignación del panel",
@@ -460,25 +465,25 @@
         "no-image": "Ninguna imagen seleccionada",
         "drop-image": "Colocar una imagen o hacer clic para seleccionar un archivo para cargar.",
         "settings": "Configuración",
-        "columns-count": "Conteo de columnas",
-        "columns-count-required": "Conteo de columnas es requerido.",
-        "min-columns-count-message": "Solamente contar 10 columnas como mínimo es permitido",
-        "max-columns-count-message": "Solamente contar 1000 columnas como máximo es permitiddo",
+        "columns-count": "Número de columnas",
+        "columns-count-required": "Número de columnas es requerido.",
+        "min-columns-count-message": "Sólo está permitido 10 columnas como mínimo.",
+        "max-columns-count-message": "Sólo está permitido 1000 columnas como máximo.",
         "widgets-margins": "Margen entre widgets",
         "horizontal-margin": "Margen horizontal",
         "horizontal-margin-required": "El valor del margen horizontal es requerido.",
-        "min-horizontal-margin-message": "Solamente es permitido el 0 como valor mínimo para el margen horizontal",
-        "max-horizontal-margin-message": "Solamente es permitido el 50 como valor máximo para el margen horizontal",
+        "min-horizontal-margin-message": "Sólo está permitido el 0 como valor mínimo para el margen horizontal",
+        "max-horizontal-margin-message": "Sólo está permitido el 50 como valor máximo para el margen horizontal",
         "vertical-margin": "Margen vertical",
         "vertical-margin-required": "El valor del margen vertical es requerido.",
-        "min-vertical-margin-message": "Solamente es permitido el 0 como valor mínimo para el margen vertical.",
-        "max-vertical-margin-message": "Solamente es permitido el 50 como valor máximo para el margen vertical",
+        "min-vertical-margin-message": "Sólo está permitido el 0 como valor mínimo para el margen vertical.",
+        "max-vertical-margin-message": "Sólo está permitido el 50 como valor máximo para el margen vertical",
         "autofill-height": "Llenado automático de altura de diseño",
         "mobile-layout": "Configuración de diseño para móvil",
         "mobile-row-height": "Altura de fila para móvil, píxel",
         "mobile-row-height-required": "Altura de fila para móvil es requerida.",
-        "min-mobile-row-height-message": "Solamente es permitido 5 píxeles como valor mínimo de altura de fila para móvil.",
-        "max-mobile-row-height-message": "Solamente es permitido 200 píxeles como valor máximo de altura de fila para móvil.",
+        "min-mobile-row-height-message": "Sólo está permitido 5 píxeles como valor mínimo en la altura de fila para móvil.",
+        "max-mobile-row-height-message": "Sólo está permitido 200 píxeles como valor máximo en la altura de fila para móvil.",
         "display-title": "Mostrar título del panel",
         "toolbar-always-open": "Mantener la barra de herramientas abierta",
         "title-color": "Color del título",
@@ -550,7 +555,12 @@
         "alarm-fields-required": "Campos de alarma son requeridos.",
         "function-types": "Tipos de funciones",
         "function-types-required": "Tipos de funciones son requeridos.",
-        "maximum-function-types": "Máximo { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }"
+        "maximum-function-types": "Máximo { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }",
+        "time-description": "timestamp of the current value;",
+        "value-description": "the current value;",
+        "prev-value-description": "result of the previous function call;",
+        "time-prev-description": "timestamp of the previous value;",
+        "prev-orig-value-description": "original previous value;"
     },
     "datasource": {
         "type": "Tipo de fuente de datos",
@@ -558,23 +568,23 @@
         "add-datasource-prompt": "Por favor agregue fuente de datos"
     },
     "details": {
-        "details": "Detalles",
-        "edit-mode": "Modo de edición"
+        "edit-mode": "Modo de edición",
+        "toggle-edit-mode": "Cambiar mode de edición"
     },
     "device": {
         "device": "Dispositivo",
         "device-required": "Dispositivo es requerido.",
         "devices": "Dispositivos",
         "management": "Gestión del dispositivo",
-        "view-devices": "Ver Dispositivos",
+        "view-devices": "Ver dispositivos",
         "device-alias": "Alias del dispositivo",
         "aliases": "Alias de los dispositivos",
         "no-alias-matching": "'{{alias}}' no encontrado.",
-        "no-aliases-found": "Alias no encontrados.",
+        "no-aliases-found": "No se encontraron alias.",
         "no-key-matching": "'{{key}}' no encontrado.",
         "no-keys-found": "Claves no encontradas.",
-        "create-new-alias": "Crear uno nuevo!",
-        "create-new-key": "Crear una nueva!",
+        "create-new-alias": "¡Crear uno nuevo!",
+        "create-new-key": "¡Crear una nueva!",
         "duplicate-alias-error": "Alias duplicado encontrado '{{alias}}'.<br>Los alias del dispositivo deben ser únicos dentro del panel.",
         "configure-alias": "Configurar '{{alias}}' alias",
         "no-devices-matching": "Dispositivos que coincidan con '{{entity}}' no fueron encontrados.",
@@ -591,10 +601,10 @@
         "add": "Agregar Dispositivo",
         "assign-to-customer": "Asignar al cliente",
         "assign-device-to-customer": "Asignar Dispositivo(s) Al Cliente",
-        "assign-device-to-customer-text": "Por favor seleccionar los dispositivos para asignar al cliente",
+        "assign-device-to-customer-text": "Por favor selecciona los dispositivos para asignar al cliente",
         "make-public": "Hacer público el dispositivo",
         "make-private": "Hacer privado el dispositivo",
-        "no-devices-text": "Dispositivos no encontrados",
+        "no-devices-text": "No se encontraron dispositivos",
         "assign-to-customer-text": "Por favor seleccionar el cliente para asignar el(los) dispositivo(s)",
         "device-details": "Detalles del dispositivo",
         "add-device-text": "Agregar nuevo dispositivo",
@@ -602,11 +612,11 @@
         "manage-credentials": "Gestionar credenciales",
         "delete": "Eliminar dispositivo",
         "assign-devices": "Asignar dispositivos",
-        "assign-devices-text": "Asignar { count, plural, 1 {1 device} other {# devices} } al cliente",
+        "assign-devices-text": "Asignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } al cliente",
         "delete-devices": "Eliminar dispositivos",
         "unassign-from-customer": "Anular asignación del cliente",
         "unassign-devices": "Anular asignación de dispositivos",
-        "unassign-devices-action-title": "Anular asignación { count, plural, 1 {1 device} other {# devices} } del cliente",
+        "unassign-devices-action-title": "Anular asignación { count, plural, 1 {1 dispositivo} other {# dispositivos} } del cliente",
         "assign-new-device": "Asignar nuevo dispositivo",
         "make-public-device-title": "¿Está seguro de que desea hacer el dispositivo '{{deviceName}}' público?",
         "make-public-device-text": "Después de la confirmación, el dispositivo y todos sus datos se harán públicos y accesibles por otros.",
@@ -614,14 +624,14 @@
         "make-private-device-text": "Después de la confirmación, el dispositivo y todos sus datos se harán privados y no serán accesibles para otros.",
         "view-credentials": "Ver credenciales",
         "delete-device-title": "¿Está seguro de que desea hacer el dispositivo '{{deviceName}}'?",
-        "delete-device-text": "Tener cuidado, después de la confirmación, el dispositivo y todos sus datos relacionados se volverán irrecuperables.",
-        "delete-devices-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 device} other {# devices} }?",
-        "delete-devices-action-title": "Eliminar { count, plural, 1 {1 device} other {# devices} }",
-        "delete-devices-text": "Tener cuidado, después de la confirmación, todos los dispositivos seleccionados serán eliminados y todos los datos relacionados se volverán irrecuperables.",
+        "delete-device-text": "¡Cuidado! Después de la confirmación, el dispositivo y todos sus datos relacionados serán irrecuperables.",
+        "delete-devices-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
+        "delete-devices-action-title": "Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }",
+        "delete-devices-text": "¡Cuidado! Después de la confirmación, todos los dispositivos seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.",
         "unassign-device-title": "¿Está seguro de que desea anular la asignación del dispositivo '{{deviceName}}'?",
         "unassign-device-text": "Después de la confirmación, se anulará asignación del dispositivo y no será accesible por el cliente.",
         "unassign-device": "Anular asignación del dispositivo",
-        "unassign-devices-title": "¿Está seguro de que desea anular asignación { count, plural, 1 {1 device} other {# devices} }?",
+        "unassign-devices-title": "¿Está seguro de que desea anular asignación { count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
         "unassign-devices-text": "Después de la confirmación, se anulará asignación de todos los dispositivos seleccionados y no serán accesibles por el cliente.",
         "device-credentials": "Credenciales del dispositivo",
         "credentials-type": "Tipo de credenciales",
@@ -635,7 +645,7 @@
         "device-type": "Tipo de dispositivo",
         "device-type-required": "Tipo de dispositivo es requerido.",
         "select-device-type": "Seleccionar tipo de dispositivo",
-        "enter-device-type": "Ingresar tipo de dispositivo",
+        "enter-device-type": "Teclee tipo de dispositivo",
         "any-device": "Algún dispositivo",
         "no-device-types-matching": "Tipos de dispositivos que coincidan con '{{entitySubtype}}' no fueron encontrados.",
         "device-type-list-empty": "No se seleccionaron tipos de dispositivos.",
@@ -661,7 +671,7 @@
         "close": "Cerrar diálogo"
     },
     "error": {
-        "unable-to-connect": "No se puede conectar al servidor! Por favor revise su conexión a Internet.",
+        "unable-to-connect": "¡No se puede conectar al servidor! Por favor, revise su conexión a Internet.",
         "unhandled-error-code": "Código de error no controlado: {{errorCode}}",
         "unknown-error": "Error desconocido"
     },
@@ -671,7 +681,7 @@
         "aliases": "Alias de las entidades",
         "entity-alias": "Alias de la entidad",
         "unable-delete-entity-alias-title": "No se puede borrar alias de la entidad",
-        "unable-delete-entity-alias-text": "Alias de la entidad '{{entityAlias}}' no piuede ser eliminado porque es usado por los siguientes widget(s):<br/>{{widgetsList}}",
+        "unable-delete-entity-alias-text": "Alias de la entidad '{{entityAlias}}' no se puede eliminar porque es usado por los siguientes widget(s):<br/>{{widgetsList}}",
         "duplicate-alias-error": "Alias duplicado fue encontrado '{{alias}}'.<br>Alias de las entidades deben ser únicos dentro del panel.",
         "missing-entity-filter-error": "Falta filtro para el alias '{{alias}}'.",
         "configure-alias": "Configurar '{{alias}}' alias",
@@ -684,58 +694,58 @@
         "entity-types": "Tipos de entidades",
         "entity-type-list": "Lista de tipos de entidades",
         "any-entity": "Alguna entidad",
-        "enter-entity-type": "Ingresara tipo de entidad",
-        "no-entities-matching": "Entidades que coincidan con '{{entity}}' no fueron encontradas.",
-        "no-entity-types-matching": "Tipos de entidades que coincidan con '{{entityType}}' no fueron encontrados.",
+        "enter-entity-type": "Teclee tipo de entidad",
+        "no-entities-matching": "No se encontraron entidades que coincidan con '{{entity}}'.",
+        "no-entity-types-matching": "No se encontraron tipos de entidades que coincidan con '{{entityType}}'.",
         "name-starts-with": "El nombre comienza con",
         "use-entity-name-filter": "Utilizar filtro",
         "entity-list-empty": "Entidades no seleccionadas.",
         "entity-type-list-empty": "Tipos de entidades no seleccionados.",
         "entity-name-filter-required": "Filtro del nombre de la entidad es requerido.",
-        "entity-name-filter-no-entity-matched": "Entidades que comienzan con  '{{entity}}' no fueron encontradas.",
+        "entity-name-filter-no-entity-matched": "No se encontraron entidades que comienzan con  '{{entity}}'.",
         "all-subtypes": "Todas",
         "select-entities": "Seleccionar entidades",
-        "no-aliases-found": "Alias no encontrados.",
+        "no-aliases-found": "No se encontraron alias.",
         "no-alias-matching": "'{{alias}}' no encontrado.",
-        "create-new-alias": "Crear uno nuevo!",
+        "create-new-alias": "¡Crear uno nuevo!",
         "key": "Clave",
         "key-name": "Nombre de clave",
-        "no-keys-found": "Claves no encontradas.",
+        "no-keys-found": "No se encontraron claves.",
         "no-key-matching": "'{{key}}' no encontrada.",
-        "create-new-key": "Crear una nueva!",
+        "create-new-key": "¡Crear una nueva!",
         "type": "Tipo",
         "type-required": "Tipo de entidad es requerido.",
         "type-device": "Dispositivo",
         "type-devices": "Dispositivos",
-        "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }",
+        "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista de # dispositivos} }",
         "device-name-starts-with": "Dispositivos cuyos nombres comienzan con '{{prefix}}'",
         "type-asset": "Activo",
         "type-assets": "Activos",
-        "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }",
+        "list-of-assets": "{ count, plural, 1 {Un activo} other {Lista de # activos} }",
         "asset-name-starts-with": "Activos cuyos nombres comienzan con '{{prefix}}'",
-        "type-entity-view": "Entity View",
-        "type-entity-views": "Entity Views",
-        "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }",
-        "entity-view-name-starts-with": "Entity Views whose names start with '{{prefix}}'",
+        "type-entity-view": "Vista de entidad",
+        "type-entity-views": "Vistas de entidad",
+        "list-of-entity-views": "{ count, plural, 1 {Una vista de entidad} other {Lista de # vistas de entidad} }",
+        "entity-view-name-starts-with": "Vistas de entidad cuyos nombres que comienzan con '{{prefix}}'",
         "type-rule": "Regla",
         "type-rules": "Reglas",
-        "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }",
+        "list-of-rules": "{ count, plural, 1 {Una regla} other {Lista de # reglas} }",
         "rule-name-starts-with": "Reglas cuyos nombres comienzan con '{{prefix}}'",
         "type-plugin": "Complemento",
         "type-plugins": "Complementos",
-        "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }",
+        "list-of-plugins": "{ count, plural, 1 {Un complemento} other {Lista de # complementos} }",
         "plugin-name-starts-with": "Complementos cuyos nombres comienzan con '{{prefix}}'",
         "type-tenant": "Organización",
         "type-tenants": "Organizaciones",
-        "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }",
+        "list-of-tenants": "{ count, plural, 1 {Una organización} other {Lista de # organizaciones} }",
         "tenant-name-starts-with": "Organizaciones cuyos nombres comienzan con '{{prefix}}'",
         "type-customer": "Cliente",
         "type-customers": "Clientes",
-        "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }",
+        "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista de # clientes} }",
         "customer-name-starts-with": "Clientes cuyos nombres comienzan con '{{prefix}}'",
         "type-user": "Usuario",
         "type-users": "Usuarios",
-        "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }",
+        "list-of-users": "{ count, plural, 1 {Un usuario} other {Lista de # usuarios} }",
         "user-name-starts-with": "Usuarios cuyos nombres comienzan con '{{prefix}}'",
         "type-dashboard": "Panel",
         "type-dashboards": "Paneles",
@@ -743,103 +753,115 @@
         "dashboard-name-starts-with": "Paneles cuyos nombres comienzan con '{{prefix}}'",
         "type-alarm": "Alarma",
         "type-alarms": "Alarmas",
-        "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }",
+        "list-of-alarms": "{ count, plural, 1 {Una alarma} other {Lista de # alarmas} }",
         "alarm-name-starts-with": "Alarmas cuyos nombres comienzan con '{{prefix}}'",
         "type-rulechain": "Cadena de reglas",
         "type-rulechains": "Cadenas de reglas",
-        "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }",
-        "rulechain-name-starts-with": "Cadenas reglas cuyos nombres comienzan con '{{prefix}}'",
+        "list-of-rulechains": "{ count, plural, 1 {Una cadena de reglas} other {Lista de # cadenas de reglas} }",
+        "rulechain-name-starts-with": "Cadenas de reglas cuyos nombres comienzan con '{{prefix}}'",
         "type-rulenode": "Nodo de reglas",
         "type-rulenodes": "Nodos de reglas",
-        "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }",
+        "list-of-rulenodes": "{ count, plural, 1 {Un nodo de reglas} other {Lista de # nodos de reglas} }",
         "rulenode-name-starts-with": "Nodos de reglas cuyos nombres comienzan con '{{prefix}}'",
         "type-current-customer": "Cliente Actual",
         "search": "Buscar entidades",
-        "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } seleccionadas",
+        "selected-entities": "{ count, plural, 1 {1 entidad} other {# entidades} } seleccionadas",
         "entity-name": "Nombre de la entidad",
         "details": "Detalles de la entidad",
         "no-entities-prompt": "Entidades no encontradas",
         "no-data": "No hay datos para mostrar",
-        "columns-to-display": "Columns to Display"
+        "columns-to-display": "Columnas a mostrar"
     },
     "entity-view": {
-        "entity-view": "Entity View",
-        "entity-views": "Entity Views",
-        "management": "Entity View management",
-        "view-entity-views": "View Entity Views",
-        "entity-view-alias": "Entity View alias",
-        "aliases": "Entity View aliases",
-        "no-alias-matching": "'{{alias}}' not found.",
-        "no-aliases-found": "No aliases found.",
-        "no-key-matching": "'{{key}}' not found.",
-        "no-keys-found": "No keys found.",
-        "create-new-alias": "Create a new one!",
-        "create-new-key": "Create a new one!",
-        "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity View aliases must be unique whithin the dashboard.",
-        "configure-alias": "Configure '{{alias}}' alias",
-        "no-entity-views-matching": "No entity views matching '{{entity}}' were found.",
+        "entity-view": "Vista de entidad",
+        "entity-view-required": "Vista de entidad es requerido.",
+        "entity-views": "Vistas de entidad",
+        "management": "Gestión de vistas de entidad",
+        "view-entity-views": "Ver vista de entidad",
+        "entity-view-alias": "Alias de vista de entidad",
+        "aliases": "Alias de vista de entidad",
+        "no-alias-matching": "'{{alias}}' no encontrado.",
+        "no-aliases-found": "No se encontraron alias.",
+        "no-key-matching": "'{{key}}' no encontrado.",
+        "no-keys-found": "No se encontraron claves.",
+        "create-new-alias": "¡Crear un nuevo!",
+        "create-new-key": "¡Crear una nueva!",
+        "duplicate-alias-error": "Alias duplicado'{{alias}}'.<br>El alias de la vista de entidad debe ser único en el dashboard.",
+        "configure-alias": "Configurar alias '{{alias}}'",
+        "no-entity-views-matching": "No se encontraron vistas que coincidan con '{{entity}}'.",
         "alias": "Alias",
-        "alias-required": "Entity View alias is required.",
-        "remove-alias": "Remove entity view alias",
-        "add-alias": "Add entity view alias",
-        "name-starts-with": "Entity View name starts with",
-        "entity-view-list": "Entity View list",
-        "use-entity-view-name-filter": "Use filter",
-        "entity-view-list-empty": "No entity views selected.",
-        "entity-view-name-filter-required": "Entity view name filter is required.",
-        "entity-view-name-filter-no-entity-view-matched": "No entity views starting with '{{entityView}}' were found.",
-        "add": "Add Entity View",
-        "assign-to-customer": "Assign to customer",
-        "assign-entity-view-to-customer": "Assign Entity View(s) To Customer",
-        "assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer",
-        "no-entity-views-text": "No entity views found",
-        "assign-to-customer-text": "Please select the customer to assign the entity view(s)",
-        "entity-view-details": "Entity view details",
-        "add-entity-view-text": "Add new entity view",
-        "delete": "Delete entity view",
-        "assign-entity-views": "Assign entity views",
-        "assign-entity-views-text": "Assign { count, plural, 1 {1 entityView} other {# entityViews} } to customer",
-        "delete-entity-views": "Delete entity views",
-        "unassign-from-customer": "Unassign from customer",
-        "unassign-entity-views": "Unassign entity views",
-        "unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entityView} other {# entityViews} } from customer",
-        "assign-new-entity-view": "Assign new entity view",
-        "delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?",
-        "delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.",
-        "delete-entity-views-title": "Are you sure you want to entity view { count, plural, 1 {1 entityView} other {# entityViews} }?",
-        "delete-entity-views-action-title": "Delete { count, plural, 1 {1 entityView} other {# entityViews} }",
-        "delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.",
-        "unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?",
-        "unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.",
-        "unassign-entity-view": "Unassign entity view",
-        "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entityView} other {# entityViews} }?",
-        "unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.",
-        "entity-view-type": "Entity View type",
-        "entity-view-type-required": "Entity View type is required.",
-        "select-entity-view-type": "Select entity view type",
-        "enter-entity-view-type": "Enter entity view type",
-        "any-entity-view": "Any entity view",
-        "no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.",
-        "entity-view-type-list-empty": "No entity view types selected.",
-        "entity-view-types": "Entity View types",
-        "name": "Name",
-        "name-required": "Name is required.",
-        "description": "Description",
-        "events": "Events",
-        "details": "Details",
-        "copyId": "Copy entity view Id",
-        "assignedToCustomer": "Assigned to customer",
-        "unable-entity-view-device-alias-title": "Unable to delete entity view alias",
-        "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
-        "select-entity-view": "Select entity view",
-        "make-public": "Make entity view public",
-        "start-ts": "Start time",
-        "end-ts": "End time",
-        "date-limits": "Date limits",
-        "client-attributes": "Client attributes",
-        "shared-attributes": "Shared attributes",
-        "server-attributes": "Server attributes",
-        "timeseries": "Timeseries"
+        "alias-required": "Alias de vista de entidad es requerido.",
+        "remove-alias": "Borrar alias de la vista de entidad",
+        "add-alias": "Añadir alias a la vista de entidad",
+        "name-starts-with": "Nombre de vista de entidad comienza con",
+        "entity-view-list": "Lista de vistas de entidad",
+        "use-entity-view-name-filter": "Usar el filtro",
+        "entity-view-list-empty": "No hay vistas de entidad seleccionadas.",
+        "entity-view-name-filter-required": "Nombre del filtro de vista de entidad es requerido.",
+        "entity-view-name-filter-no-entity-view-matched": "No se econtraron vistas de entidad que comiencen con '{{entityView}}'.",
+        "add": "Añadir vista de entidad",
+        "assign-to-customer": "Asignar a cliente",
+        "assign-entity-view-to-customer": "Asignar vista de entidad a cliente",
+        "assign-entity-view-to-customer-text": "Por favor, seleccione las vistas de entidad para asignar al cliente",
+        "no-entity-views-text": "No se encontraron vistas de entidad",
+        "assign-to-customer-text": "Por favor, seleccione el cliente para asignar la vista de entidad",
+        "entity-view-details": "Detalles de la vista de entidad",
+        "add-entity-view-text": "Añadir nueva vista de entidad",
+        "delete": "Borrar vista de entidad",
+        "assign-entity-views": "Asignar vistas de entidad",
+        "assign-entity-views-text": "Asignar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } a cliente",
+        "delete-entity-views": "Borrar vistas de entidad",
+        "unassign-from-customer": "Anular asignación a cliente",
+        "unassign-entity-views": "Anular asignación de vistas de entidad",
+        "unassign-entity-views-action-title": "Anular asignación { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } al cliente",
+        "assign-new-entity-view": "Asignar nueva vista de entidad",
+        "delete-entity-view-title": "¿Está seguro que quiere borrar la vista de entidad '{{entityViewName}}'?",
+        "delete-entity-view-text": "¡Cuidado! Después de la confirmación, la vista de la entidad y todos los datos relacionados serán irrecuperables.",
+        "delete-entity-views-title": "¿Está seguro que quiere borrar las vistas de entidad { count, plural, 1 {1 entityView} other {# entityViews} }?",
+        "delete-entity-views-action-title": "Borrar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }",
+        "delete-entity-views-text": "¡Cuidado! Después de la confirmación, todas las vistas de entidades seleccionadas se eliminarán y todos los datos relacionados serán irrecuperables.",
+        "unassign-entity-view-title": "¿Está seguro que quiere anular la asignación de la vista de entidad '{{entityViewName}}'?",
+        "unassign-entity-view-text": "Después de la confirmación, la vista de la entidad quedará sin asignar y el cliente no podrá acceder a ella.",
+        "unassign-entity-view": "Anular asignación de la vista de entidad",
+        "unassign-entity-views-title": "¿Está seguro que quiere anular la asignación de { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?",
+        "unassign-entity-views-text": "Después de la confirmación, todas las vistas de entidades seleccionadas quedarán sin asignar y el cliente no podrá acceder a ellas.",
+        "entity-view-type": "Tipo de vista de entidad",
+        "entity-view-type-required": "Tipo de vista de entidad es requerido.",
+        "select-entity-view-type": "Seleccione el tipo de vista de entidad",
+        "enter-entity-view-type": "Teclee el tipo de vista de entidad",
+        "any-entity-view": "Cualquier vista de entidad",
+        "no-entity-view-types-matching": "No se encontraron typos de vista de entidad '{{entitySubtype}}'.",
+        "entity-view-type-list-empty": "No hay tipos de vista de entidad seleccionados.",
+        "entity-view-types": "Tipos de vista de entidad",
+        "name": "Nombre",
+        "name-required": "Nombre es requerido.",
+        "description": "Descripción",
+        "events": "Eventos",
+        "details": "Detalles",
+        "copyId": "Copiar el Id de la vista de entidad",
+        "assignedToCustomer": "Asignado a cliente",
+        "unable-entity-view-device-alias-title": "No se puede eliminar el alias de vista de entidad",
+        "unable-entity-view-device-alias-text": "El alias del dispositivo '{{entityViewAlias}}' no se puede borrar porque está siendo usado por el widget(s):<br/>{{widgetsList}}",
+        "select-entity-view": "Seleccionar vista de entidad",
+        "make-public": "Hacer pública la vista de entidad",
+        "start-date": "Fecha de inicio",
+        "start-ts": "Tiempo de inicio",
+        "end-date": "Fecha de finalización",
+        "end-ts": "Tiempo de finalización",
+        "date-limits": "Limites de fecha",
+        "client-attributes": "Atributos de cliente",
+        "shared-attributes": "Atributos compartidos",
+        "server-attributes": "Atributos de servidor",
+        "timeseries": "Series de tiempo",
+        "client-attributes-placeholder": "Atributos de cliente",
+        "shared-attributes-placeholder": "Atributos compartidos",
+        "server-attributes-placeholder": "Atributos de servidor",
+        "timeseries-placeholder": "Series de tiempo",
+        "target-entity": "Entidad objetivo",
+        "attributes-propagation": "Propagación de atributos",
+        "attributes-propagation-hint": "La vista de entidad copiará automáticamente los atributos especificados de la entidad de destino cada vez que guarde o actualice esta vista de entidad. Por razones de rendimiento, los atributos de entidad objetivo no se propagan a la vista de entidad en cada cambio de atributo. Puede habilitar la propagación automática configurando el nodo de la regla \"copiar a la vista\" en su cadena de reglas y vincular los mensajes \"Atributos de la publicación\" y \"Atributos actualizados\" al nuevo nodo de la regla.",
+        "timeseries-data": "Datos de series de tiempo",
+        "timeseries-data-hint": "Configure las claves de los datos de las series de tiempo de la entidad de destino que serán accesibles para la vista de la entidad. Los datos de esta serie temporal son de solo lectura."
     },
     "event": {
         "event-type": "Tipo de evento",
@@ -848,7 +870,7 @@
         "type-stats": "Estadísticas",
         "type-debug-rule-node": "Depurar",
         "type-debug-rule-chain": "Depurar",
-        "no-events-prompt": "Eventos no encontrados",
+        "no-events-prompt": "No se encontraron eventos",
         "error": "Error",
         "alarm": "Alarma",
         "event-time": "Tiempo del evento",
@@ -865,14 +887,14 @@
         "data": "Datos",
         "event": "Evento",
         "status": "Estado",
-        "success": "Éxito",
-        "failed": "Falla",
+        "success": "Correcto",
+        "failed": "Erróneo",
         "messages-processed": "Mensajes procesados",
         "errors-occurred": "Errores ocurridos"
     },
     "extension": {
         "extensions": "Extensiones",
-        "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } seleccionadas",
+        "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensiones} } seleccionadas",
         "type": "Tipo",
         "key": "Clave",
         "value": "Valor",
@@ -885,9 +907,9 @@
         "add": "Agregar extensión",
         "edit": "Editar extensión",
         "delete-extension-title": "¿Está seguro de que desea eliminar la extensión '{{extensionId}}'?",
-        "delete-extension-text": "Tener cuidado, después de la confirmación, la extensión y todos los datos relacionados se volverán irrecuperables.",
-        "delete-extensions-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 extension} other {# extensions} }?",
-        "delete-extensions-text": "Tener cuidado, después de la confirmación, se eliminarán todas las extensiones seleccionadas.",
+        "delete-extension-text": "¡Cuidado! Después de la confirmación, la extensión y todos los datos relacionados serán irrecuperables.",
+        "delete-extensions-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 extension} other {# extensiones} }?",
+        "delete-extensions-text": "¡Cuidado! Después de la confirmación, se eliminarán todas las extensiones seleccionadas.",
         "converters": "Conversores",
         "converter-id": "ID del conversor",
         "configuration": "Configuración",
@@ -986,6 +1008,8 @@
         "modbus-add-server": "Agregar servidor/esclavo",
         "modbus-add-server-prompt": "Por favor agregar servidor/esclavo",
         "modbus-transport": "Transporte",
+        "modbus-tcp-reconnect": "Reconexión automática",
+        "modbus-rtu-over-tcp": "RTU sobre TCP",
         "modbus-port-name": "Nombre del puerto serial",
         "modbus-encoding": "Codificación",
         "modbus-parity": "Paridad",
@@ -1010,7 +1034,6 @@
         "modbus-register-count": "Contador del registro",
         "modbus-register-count-range": "Contador del registro debe ser un valor positivo.",
         "modbus-byte-order": "Orden del byte",
-
         "sync": {
             "status": "Estado",
             "sync": "Sincronización",
@@ -1018,7 +1041,6 @@
             "last-sync-time": "Último tiempo de sincroniación",
             "not-available": "No disponible"
         },
-
         "export-extensions-configuration": "Exportar configuración de extensiones",
         "import-extensions-configuration": "Importar configuración de extensiones",
         "import-extensions": "Importar extensiones",
@@ -1038,12 +1060,12 @@
     },
     "grid": {
         "delete-item-title": "¿Está seguro de que desea eliminar este ítem?",
-        "delete-item-text": "Tener cuidado, después de la confirmación, este ítem y todos los datos relacionados se volverán irrecuperables.",
-        "delete-items-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 item} other {# items} }?",
-        "delete-items-action-title": "Eliminar { count, plural, 1 {1 item} other {# items} }",
-        "delete-items-text": "Tener cuidado, después de la confirmación se eliminarán todos los ítems seleccionados y todos los datos relacionados se volverán irrecuperables.",
+        "delete-item-text": "¡Cuiado! Después de la confirmación, este ítem y todos los datos relacionados serán irrecuperables.",
+        "delete-items-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 ítem} other {# ítems} }?",
+        "delete-items-action-title": "Eliminar { count, plural, 1 {1 ítem} other {# ítems} }",
+        "delete-items-text": "¡Cuidado! Después de la confirmación se eliminarán todos los ítems seleccionados y todos los datos relacionados serán irrecuperables.",
         "add-item-text": "Agregar nuevo ítem",
-        "no-items-text": "Ítems no encontrados",
+        "no-items-text": "No se encontraron ítems",
         "item-details": "Detalles del ítem",
         "delete-item": "Eliminar ítem",
         "delete-items": "Eliminar ítems",
@@ -1070,7 +1092,7 @@
     "js-func": {
         "no-return-error": "La función debe devolver el valor!",
         "return-type-mismatch": "La función debe devolver el valor de '{{type}}' type!",
-        "tidy": "Ordenado"
+        "tidy": "Formatear"
     },
     "key-val": {
         "key": "Clave",
@@ -1102,19 +1124,19 @@
     },
     "login": {
         "login": "Iniciar sesión",
-        "request-password-reset": "Solicitar Restablecimiento Contraseña",
-        "reset-password": "Restablecer Contraseña",
-        "create-password": "Crear Contraseña",
-        "passwords-mismatch-error": "Las contraseñas introducidas deben ser las mismas!",
-        "password-again": "Contraseña nuevamente",
-        "sign-in": "Por favor registrarse",
+        "request-password-reset": "Restablecer contraseña",
+        "reset-password": "Restablecer contraseña",
+        "create-password": "Crear contraseña",
+        "passwords-mismatch-error": "¡Las contraseñas introducidas deben ser iguales!",
+        "password-again": "Repita la contraseña de nuevo",
+        "sign-in": "Por favor registrese",
         "username": "Nombre de usuario (correo electrónico)",
         "remember-me": "Recordarme",
-        "forgot-password": "Olvidó Contraseña?",
-        "password-reset": "Restablecimiento de Contraseña",
+        "forgot-password": "¿Olvidó la contraseña?",
+        "password-reset": "Restablecer contraseña",
         "new-password": "Nueva contraseña",
-        "new-password-again": "Nueva contraseña otra vez",
-        "password-link-sent-message": "El enlace para el restablecimieneto de la contraseña fue enviado exitosamente!",
+        "new-password-again": "Repita la nueva contraseña",
+        "password-link-sent-message": "¡El enlace para el restablecer la contraseña fue enviado correctamente!",
         "email": "Correo electrónico"
     },
     "position": {
@@ -1141,7 +1163,7 @@
         },
         "from-relations": "Relaciones salientes",
         "to-relations": "Relaciones entrantes",
-        "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selecciondas",
+        "selected-relations": "{ count, plural, 1 {1 relación} other {# relaciones} } selecciondas",
         "type": "Tipo",
         "to-entity-type": "Hacia tipo de entidad",
         "to-entity-name": "Hacia nombre de entidad",
@@ -1156,13 +1178,13 @@
         "add": "Agregar relación",
         "edit": "Editar relación",
         "delete-to-relation-title": "¿Está seguro de que desea eliminar la relación hacia la entidad '{{entityName}}'?",
-        "delete-to-relation-text": "Tener cuidado, después de la confirmación, la entidad '{{entityName}}' no estará relacionada desde la entidad actual.",
-        "delete-to-relations-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 relation} other {# relations} }?",
-        "delete-to-relations-text": "Tener cuidado, después de la confirmación, se eliminarán todas las relaciones seleccionadas y las entidades correspondientes no estarán relacionadas desde la entidad actual.",
+        "delete-to-relation-text": "¡Cuidado! Después de la confirmación, la entidad '{{entityName}}' no estará relacionada desde la entidad actual.",
+        "delete-to-relations-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 relación} other {# relaciones} }?",
+        "delete-to-relations-text": "¡Cuidado! Después de la confirmación, se eliminarán todas las relaciones seleccionadas y las entidades correspondientes no estarán relacionadas desde la entidad actual.",
         "delete-from-relation-title": "¿Está seguro de que desea eliminar la relación desde la entidad '{{entityName}}'?",
-        "delete-from-relation-text": "Tener cuidado, después de la confirmación, la entidad actual no será relacionada desde la entidad '{{entityName}}'.",
-        "delete-from-relations-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 relation} other {# relations} }?",
-        "delete-from-relations-text": "Tener cuidado, después de la confirmación, se eliminarán todas las relaciones seleccionadas y la entidad actual no será relacionada desde las correspondientes entidades.",
+        "delete-from-relation-text": "¡Cuidado! Después de la confirmación, la entidad actual no será relacionada desde la entidad '{{entityName}}'.",
+        "delete-from-relations-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 relación} other {# relaciones} }?",
+        "delete-from-relations-text": "¡Cuidado! Después de la confirmación, se eliminarán todas las relaciones seleccionadas y la entidad actual no será relacionada desde las correspondientes entidades.",
         "remove-relation-filter": "Eliminar filtro de relación",
         "add-relation-filter": "Agregar filtro de relación",
         "any-relation": "Alguna relación",
@@ -1183,10 +1205,10 @@
         "set-root-rulechain-title": "¿Está seguro de que desea hacer la cadena de reglas '{{ruleChainName}}' root?",
         "set-root-rulechain-text": "Después de la confirmación, la cadena de reglas se volverá raíz y manejará todos los mensajes de transporte entrantes.",
         "delete-rulechain-title": "¿Está seguro de que desea eliminar la cadena de reglas '{{ruleChainName}}'?",
-        "delete-rulechain-text": "Tener cuidado, después de la confirmación, la cadena de reglas y todos los datos relacionados se volverán irrecuperables.",
-        "delete-rulechains-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 rule chain} other {# rule chains} }?",
-        "delete-rulechains-action-title": "Eliminar { count, plural, 1 {1 rule chain} other {# rule chains} }",
-        "delete-rulechains-text": "Tener cuidado, después de la confirmación se eliminarán todas las cadenas de reglas seleccionadas y todos los datos relacionados se volverán irrecuperables.",
+        "delete-rulechain-text": "¡Cuidado! Después de la confirmación, la cadena de reglas y todos los datos relacionados serán irrecuperables.",
+        "delete-rulechains-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }?",
+        "delete-rulechains-action-title": "Eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }",
+        "delete-rulechains-text": "¡Cuidado! Después de la confirmación se eliminarán todas las cadenas de reglas seleccionadas y todos los datos relacionados serán irrecuperables.",
         "add-rulechain-text": "Agregar nueva cadena de reglas",
         "no-rulechains-text": "Cadenas de reglas no encontradas",
         "rulechain-details": "Detalles de la cadena de reglas",
@@ -1211,7 +1233,7 @@
         "details": "Detalles",
         "events": "Eventos",
         "search": "Nodos de búsqueda",
-        "open-node-library": "Abrir libreria de nodos",
+        "open-node-library": "Abrir librería de nodos",
         "add": "Agregar nodo de reglas",
         "name": "Nombre",
         "name-required": "El nombre es requerido.",
@@ -1237,7 +1259,7 @@
         "custom-link-label-required": "Etiqueta del enlace personalizado es requerida.",
         "link-labels": "Etiquetas del enlace",
         "link-labels-required": "Etiquetas del enlace son requeridas.",
-        "no-link-labels-found": "Etiquetas de enlaces no encontradas",
+        "no-link-labels-found": "No se encontraron etiquetas de enlaces",
         "no-link-label-matching": "'{{label}}' no encontrada.",
         "create-new-link-label": "Crear una nueva!",
         "type-filter": "Filtro",
@@ -1258,7 +1280,7 @@
         "type-unknown-details": "Regla de nodo no resuelta",
         "directive-is-not-loaded": "La directiva de configuración definida '{{directiveName}}' no está disponible.",
         "ui-resources-load-error": "Error al cargar los recursos de configuración ui.",
-        "invalid-target-rulechain": "No se puede resolver la cadena de reglas objetivo!",
+        "invalid-target-rulechain": "¡No se puede resolver la cadena de reglas objetivo!",
         "test-script-function": "Probar función script",
         "message": "Mensaje",
         "message-type": "Tipo de mensaje",
@@ -1274,7 +1296,7 @@
         "tenant": "Organización",
         "tenants": "Organizaciones",
         "management": "Gestión de la organización",
-        "add": "Agregar Organización",
+        "add": "Agregar organización",
         "admins": "Administradores",
         "manage-tenant-admins": "Gestionar administradores de la organización",
         "delete": "Eliminar organización",
@@ -1282,10 +1304,10 @@
         "no-tenants-text": "Organizaciones no encontradas",
         "tenant-details": "Detalles de la organización",
         "delete-tenant-title": "¿Está seguro de que desea eliminar la organización '{{tenantTitle}}'?",
-        "delete-tenant-text": "Tener cuidado, después de la confirmación, la organización y todos los datos relacionados se volverán irrecuperables.",
-        "delete-tenants-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 tenant} other {# tenants} }?",
-        "delete-tenants-action-title": "Eliminar { count, plural, 1 {1 tenant} other {# tenants} }",
-        "delete-tenants-text": "Tener cuidado, después de la confirmación se eliminarán todas las organizaciones seleccionadas y todos los datos relacionados se volverán irrecuperables.",
+        "delete-tenant-text": "¡Cuidado! Después de la confirmación, la organización y todos los datos relacionados serán irrecuperables.",
+        "delete-tenants-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 organización} other {# organizaciones} }?",
+        "delete-tenants-action-title": "Eliminar { count, plural, 1 {1 organización} other {# organizaciones} }",
+        "delete-tenants-text": "¡Cuidado! Después de la confirmación se eliminarán todas las organizaciones seleccionadas y todos los datos relacionados serán irrecuperables.",
         "title": "Título",
         "title-required": "Título es requerido.",
         "description": "Descripción",
@@ -1294,14 +1316,14 @@
         "copyId": "Copiar ID de la organización",
         "idCopiedMessage": "ID de la organización ha sido copiado al portapapeles",
         "select-tenant": "Seleccionar organización",
-        "no-tenants-matching": "Organizaciones que coincidan con '{{entity}}' no fueron encontradas.",
+        "no-tenants-matching": "No se encontraron organizaciones que coincidan con '{{entity}}'.",
         "tenant-required": "Organización es requerida"
     },
     "timeinterval": {
-        "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",
-        "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }",
-        "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }",
-        "days-interval": "{ days, plural, 1 {1 day} other {# days} }",
+        "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segundos} }",
+        "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }",
+        "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }",
+        "days-interval": "{ days, plural, 1 {1 día} other {# días} }",
         "days": "Días",
         "hours": "Horas",
         "minutes": "Minutos",
@@ -1309,17 +1331,17 @@
         "advanced": "Avanzado"
     },
     "timewindow": {
-        "days": "{ days, plural, 1 { day } other {# days } }",
-        "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }",
-        "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }",
-        "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }",
+        "days": "{ days, plural, 1 { día } other {# días } }",
+        "hours": "{ hours, plural, 0 { horas } 1 {1 hora } other {# horas } }",
+        "minutes": "{ minutes, plural, 0 { minutos } 1 {1 minuto } other {# minutos } }",
+        "seconds": "{ seconds, plural, 0 { segundos } 1 {1 segundo } other {# segundos } }",
         "realtime": "Tiempo real",
         "history": "Historia",
-        "last-prefix": "última",
-        "period": "desde {{ startTime }} to {{ endTime }}",
-        "edit": "Editat ventana de tiempo",
+        "last-prefix": "último(s)",
+        "period": "desde {{ startTime }} hasta {{ endTime }}",
+        "edit": "Editar ventana de tiempo",
         "date-range": "Rango de fecha",
-        "last": "Última",
+        "last": "Último(s)",
         "time-period": "Período de tiempo"
     },
     "user": {
@@ -1334,14 +1356,14 @@
         "add": "Agregar Usuario",
         "delete": "Eliminar usuario",
         "add-user-text": "Agregar nuevo usuario",
-        "no-users-text": "Usuarios no encontrados",
+        "no-users-text": "No se encontraron usuarios",
         "user-details": "Detalles de usuario",
         "delete-user-title": "¿Está seguro de que desea eliminar el usuario '{{userEmail}}'?",
-        "delete-user-text": "Tener cuidado, después de la confirmación, el usuario y todos los datos relacionados se volverán irrecuperables.",
-        "delete-users-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 user} other {# users} }?",
-        "delete-users-action-title": "Delete { count, plural, 1 {1 user} other {# users} }",
-        "delete-users-text": "Tener cuidado, después de la confirmación se eliminarán todas los usuarios seleccionados y todos los datos relacionados se volverán irrecuperables.",
-        "activation-email-sent-message": "Correo electrónico de activación fue enviado exitosamente!",
+        "delete-user-text": "¡Cuidado! Después de la confirmación, el usuario y todos los datos relacionados serán irrecuperables.",
+        "delete-users-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 usuario} other {# usuarios} }?",
+        "delete-users-action-title": "Borrar { count, plural, 1 {1 usuario} other {# usuarios} }",
+        "delete-users-text": "¡Cuidado! Después de la confirmación se eliminarán todas los usuarios seleccionados y todos los datos relacionados serán irrecuperables.",
+        "activation-email-sent-message": "¡Correo electrónico de activación fue enviado correctamente!",
         "resend-activation": "Reenviar activación",
         "email": "Correo electrónico",
         "email-required": "Correo electrónico es requerido.",
@@ -1358,12 +1380,12 @@
         "display-activation-link": "Mostrar enlace de activación",
         "send-activation-mail": "Enviar correo electrónico de activación",
         "activation-link": "Enlace de activación de usuario",
-        "activation-link-text": "Para activar el usuario, utilizar los siguientes <a href='{{activationLink}}' target='_blank'>activation link</a> :",
+        "activation-link-text": "Para activar el usuario, utilice el siguiente <a href='{{activationLink}}' target='_blank'>enlace de activación</a> :",
         "copy-activation-link": "Copiar enlace de activación",
         "activation-link-copied-message": "El enlace de activación de usuario ha sido copiado al portapapeles",
         "details": "Detalles",
         "login-as-tenant-admin": "Iniciar sesión como Administrador de la Organización",
-        "login-as-customer-user": "Iniciar sesión como Usuario Cliente"
+        "login-as-customer-user": "Iniciar sesión como Usuario cliente"
     },
     "value": {
         "type": "Tipo de valor",
@@ -1381,32 +1403,32 @@
         "long": "Largo"
     },
     "widget": {
-        "widget-library": "Libreria de Widgets",
-        "widget-bundle": "Paquete de Widgets",
+        "widget-library": "Librería de widgets",
+        "widget-bundle": "Paquete de widgets",
         "select-widgets-bundle": "Seleccionar paquete de widgets",
         "management": "Gestión de widget",
         "editor": "Editor de Widget",
-        "widget-type-not-found": "Problema cargando configuración de widget.<br>Probablemente tipo de widget asociado fue eliminado.",
+        "widget-type-not-found": "Problema cargando configuración de widget.<br>Probablemente el tipo de widget asociado fue eliminado.",
         "widget-type-load-error": "El widget no fue cargado debido a los siguientes errores:",
         "remove": "Eliminar widget",
         "edit": "Editar widget",
         "remove-widget-title": "¿Está seguro de que desea eliminar el widget '{{widgetTitle}}'?",
-        "remove-widget-text": "Después de la confirmación, el widget y todos los datos relacionados se volverán irrecuperables.",
+        "remove-widget-text": "Después de la confirmación, el widget y todos los datos relacionados serán irrecuperables.",
         "timeseries": "Series temporales",
         "search-data": "Buscar datos",
-        "no-data-found": "Datos no encontrados",
+        "no-data-found": "No se encontraron datos",
         "latest-values": "Últimos valores",
         "rpc": "Widget de control",
         "alarm": "Widget de alarma",
         "static": "Widget estático",
         "select-widget-type": "Seleccionar tipo de widget",
-        "missing-widget-title-error": "Título del widget debe ser especificado!",
+        "missing-widget-title-error": "¡Título del widget debe ser especificado!",
         "widget-saved": "Widget guardado",
-        "unable-to-save-widget-error": "No se puede guardar widget! El widget tiene errores!",
+        "unable-to-save-widget-error": "¡No se puede guardar widget! ¡El widget tiene errores!",
         "save": "Guardar widget",
         "saveAs": "Guardar widget como",
         "save-widget-type-as": "Guardar tipo de widget como",
-        "save-widget-type-as-text": "Por favor ingresar nuevo título del widget y/o seleccionar paquete de widgets objetivo",
+        "save-widget-type-as-text": "Por favor escriba el nuevo título del widget y/o seleccionar paquete de widgets objetivo",
         "toggle-fullscreen": "Alternar pantalla completa",
         "run": "Ejecutar widget",
         "title": "Título del widget",
@@ -1415,20 +1437,20 @@
         "resources": "Recursos",
         "resource-url": "JavaScript/CSS URL",
         "remove-resource": "Eliminar recurso",
-        "add-resource": "Agregar recursose",
+        "add-resource": "Agregar recurso",
         "html": "HTML",
-        "tidy": "Ordenado",
+        "tidy": "Formatear",
         "css": "CSS",
         "settings-schema": "Esquema de configuración",
         "datakey-settings-schema": "Esquema de configuración de clave de datos",
         "javascript": "Javascript",
         "remove-widget-type-title": "¿Está seguro de que desea eliminar el tipo de widget '{{widgetName}}'?",
-        "remove-widget-type-text": "Después de la confirmación, el tipo de widget y todos los datos relacionados se volverán irrecuperables.",
+        "remove-widget-type-text": "Después de la confirmación, el tipo de widget y todos los datos relacionados serán irrecuperables.",
         "remove-widget-type": "Eliminar tipo de widget",
         "add-widget-type": "Agregar nuevo tipo de widget",
-        "widget-type-load-failed-error": "Failed to load widget type!",
-        "widget-template-load-failed-error": "Error al cargar la plantilla del widget!",
-        "add": "Agregar Widget",
+        "widget-type-load-failed-error": "¡Error al cargar el tipo de widget!",
+        "widget-template-load-failed-error": "¡Error al cargar la plantilla del widget!",
+        "add": "Agregar widget",
         "undo": "Deshacer cambios en el widget",
         "export": "Exportar widget"
     },
@@ -1446,21 +1468,21 @@
     },
     "widgets-bundle": {
         "current": "Paquete actual",
-        "widgets-bundles": "Paquetes de Widgets",
-        "add": "Agregar Paquete de Widgets",
-        "delete": "Eliminar paquete de widget",
+        "widgets-bundles": "Paquetes de widgets",
+        "add": "Agregar paquete de widgets",
+        "delete": "Eliminar paquete de widgets",
         "title": "Título",
         "title-required": "Título es requerido.",
         "add-widgets-bundle-text": "Agregar nuevo paquete de widgets",
-        "no-widgets-bundles-text": "Paquetes de widgets no encontrados",
+        "no-widgets-bundles-text": "No se encontraron paquetes de widgets",
         "empty": "Paquete de widgets está vacío",
         "details": "Detalles",
         "widgets-bundle-details": "Detalles del paquete de widgets",
         "delete-widgets-bundle-title": "¿Está seguro de que desea eliminar el paquete de widgets '{{widgetsBundleTitle}}'?",
-        "delete-widgets-bundle-text": "Tener cuidado, después de la confirmación, el paquete de widgets y todos los datos relacionados se volverán irrecuperables.",
-        "delete-widgets-bundles-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }?",
-        "delete-widgets-bundles-action-title": "Eliminar { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }",
-        "delete-widgets-bundles-text": "Tener cuidado, después de la confirmación se eliminarán todas los paquetes de widgets seleccionados y todos los datos relacionados se volverán irrecuperables.",
+        "delete-widgets-bundle-text": "¡Cuidado! Después de la confirmación, el paquete de widgets y todos los datos relacionados serán irrecuperables.",
+        "delete-widgets-bundles-title": "¿Está seguro de que desea eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }?",
+        "delete-widgets-bundles-action-title": "Eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }",
+        "delete-widgets-bundles-text": "¡Cuidado! Después de la confirmación se eliminarán todas los paquetes de widgets seleccionados y todos los datos relacionados serán irrecuperables.",
         "no-widgets-bundles-matching": "Paquetes de widgets que coincidan con '{{widgetsBundle}}' no fueron encontrados.",
         "widgets-bundle-required": "Paquete de widgets es requerido.",
         "system": "Sistema",
@@ -1492,25 +1514,25 @@
         "units": "Símbolo especial para mostrar junto al valor.",
         "decimals": "Número de dígitos después del punto flotante",
         "timewindow": "Ventana de tiempo",
-        "use-dashboard-timewindow": "Utilizar ventana de timpo del panel",
+        "use-dashboard-timewindow": "Utilizar ventana de tiempo del panel",
         "display-legend": "Mostrar leyenda",
-        "datasources": "Fuentes de datos",
-        "maximum-datasources": "Máximo { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
+        "datasources": "Orígenes de datos",
+        "maximum-datasources": "Máximo { count, plural, 1 {1 origen de datos permitido.} other {# origenes de datos permitidos} }",
         "datasource-type": "Tipo",
-        "datasource-parameters": "Parametros",
-        "remove-datasource": "Eliminar fuente de datos",
-        "add-datasource": "Agregar fuente de datos",
+        "datasource-parameters": "Parámetros",
+        "remove-datasource": "Eliminar origen de datos",
+        "add-datasource": "Agregar origen de datos",
         "target-device": "Dispositivo objetivo",
-        "alarm-source": "Fuente de alarma",
-        "actions": "Accines",
+        "alarm-source": "Origen de la alarma",
+        "actions": "Acciones",
         "action": "Acción",
         "add-action": "Agregar acción",
         "search-actions": "Buscar acciones",
-        "action-source": "Fuente de acción",
+        "action-source": "Origen de acción",
         "action-source-required": "Fuente de acción es requerida.",
         "action-name": "Nombre",
         "action-name-required": "Nombre de acción es requerido.",
-        "action-name-not-unique": "Ya existe otra acción con el mismo nombre.<br/>El nombre de la acción debe ser único dentro de la misma fuente de acción.",
+        "action-name-not-unique": "Ya existe otra acción con el mismo nombre.<br/>El nombre de la acción debe ser único dentro del mismo orígen de acción.",
         "action-icon": "Icono",
         "action-type": "Tipo",
         "action-type-required": "Tipo de acción es requerido.",
@@ -1530,7 +1552,7 @@
     "icon": {
         "icon": "Icono",
         "select-icon": "Seleccionar icono",
-        "material-icons": "Iconos de maerial",
+        "material-icons": "Iconos de material design",
         "show-all": "Mostrar todos los iconos"
     },
     "custom": {
@@ -1544,6 +1566,7 @@
     "language": {
         "language": "Lenguaje",
         "locales": {
+            "de_DE": "Alemán",
             "fr_FR": "Francés",
             "zh_CN": "Chino",
             "en_US": "Inglés",
@@ -1555,4 +1578,4 @@
             "tr_TR": "Turco"
         }
     }
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json
index dd2fa0d..5b4edd9 100644
--- a/ui/src/app/locale/locale.constant-fr_FR.json
+++ b/ui/src/app/locale/locale.constant-fr_FR.json
@@ -1003,6 +1003,7 @@
     "language": {
         "language": "Language",
         "locales": {
+            "de_DE": "Allemand",
             "en_US": "Anglais",
             "fr_FR": "Français",
             "es_ES": "Espagnol",
@@ -1459,4 +1460,4 @@
         "widgets-bundle-required": "Un groupe de widgets est requis.",
         "widgets-bundles": "Groupes de widgets"
     }
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json
index 49f9b48..4c73548 100644
--- a/ui/src/app/locale/locale.constant-it_IT.json
+++ b/ui/src/app/locale/locale.constant-it_IT.json
@@ -100,7 +100,7 @@
             "UNACK": "Non riconosciuto"
         },
         "display-status": {
-            "ACTIVE_UNACK": "Active Unacknowledged", 
+            "ACTIVE_UNACK": "Active Unacknowledged",
             "ACTIVE_ACK": "Active Acknowledged",
             "CLEARED_UNACK": "Cleared Unacknowledged",
             "CLEARED_ACK": "Cleared Acknowledged"
@@ -109,8 +109,8 @@
         "created-time": "Orario di creazione",
         "type": "Tipo",
         "severity": "Livello di gravità",
-        "originator": "Origine", 
-        "originator-type": "Tipo origine", 
+        "originator": "Origine",
+        "originator-type": "Tipo origine",
         "details": "Dettagli",
         "status": "Stato",
         "alarm-details": "Dettagli allarme",
@@ -132,9 +132,9 @@
         "polling-interval-required": "Intervallo di polling Allarmi richiesto.",
         "min-polling-interval-message": "L'intervallo di polling deve essere di almeno 1 sec.",
         "aknowledge-alarms-title": "Conferma { count, plural, 1 {1 allarme} other {# allarmi} }",
-        "aknowledge-alarms-text": "Sei sicuro di voler confermare { count, plural, 1 {1 allarme} other {# allarmi} }?", 
+        "aknowledge-alarms-text": "Sei sicuro di voler confermare { count, plural, 1 {1 allarme} other {# allarmi} }?",
         "clear-alarms-title": "Elimina { count, plural, 1 {1 allarme} other {# allarmi} }",
-        "clear-alarms-text": "Sei sicuro di voler eliminare { count, plural, 1 {1 allarme} other {# allarmi} }?" 
+        "clear-alarms-text": "Sei sicuro di voler eliminare { count, plural, 1 {1 allarme} other {# allarmi} }?"
     },
     "alias": {
         "add": "Aggiungi alias",
@@ -910,7 +910,6 @@
         "modbus-register-count": "Register count",
         "modbus-register-count-range": "Register count should be a positive value.",
         "modbus-byte-order": "Byte order",
-
         "sync": {
             "status": "Stato",
             "sync": "Sincronizzato",
@@ -918,7 +917,6 @@
             "last-sync-time": "Ultima sincronizzazione",
             "not-available": "Non disponibile"
         },
-
         "export-extensions-configuration": "Esporta configurazione estensioni",
         "import-extensions-configuration": "Importa configurazione estensioni",
         "import-extensions": "Importa estensione",
@@ -1414,7 +1412,7 @@
         "export": "Esporta un tipo di widget",
         "export-failed-error": "Impossibile esportare il tipo di widget: {{error}}",
         "create-new-widget-type": "Crea un nuovo tipo di widget",
-        "widget-type-file": "Widget type file", 
+        "widget-type-file": "Widget type file",
         "invalid-widget-type-file-error": "Impossibile importare un tipo di widget: struttura dati del widget non valida."
     },
     "icon": {
@@ -1424,7 +1422,7 @@
         "show-all": "Mostra tutte le icone"
     },
     "custom": {
-        "widget-action": { 
+        "widget-action": {
             "action-cell-button": "Action cell button",
             "row-click": "On row click",
             "marker-click": "On marker click",
@@ -1434,7 +1432,8 @@
     "language": {
         "language": "Lingua",
         "locales": {
-			"fr_FR": "Francese",
+            "de_DE": "Tedesco",
+            "fr_FR": "Francese",
             "zh_CN": "Cinese",
             "ko_KR": "Coreano",
             "en_US": "Inglese",
@@ -1445,4 +1444,4 @@
             "tr_TR": "Turco"
         }
     }
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-ja_JA.json b/ui/src/app/locale/locale.constant-ja_JA.json
index 2ab2bcb..fea4ab3 100644
--- a/ui/src/app/locale/locale.constant-ja_JA.json
+++ b/ui/src/app/locale/locale.constant-ja_JA.json
@@ -288,11 +288,11 @@
 		"type-suspended": "一時停止中",
 		"type-credentials-read": "信用証明書を読む",
 		"type-attributes-read": "読み取られた属性",
-        "type-relation-add-or-update": "関係が更新されました",
-        "type-relation-delete": "関係が削除されました",
-        "type-relations-delete": "すべてのリレーションを削除",
-        "type-alarm-ack": "承認された",
-        "type-alarm-clear": "クリアされた",													  
+		"type-relation-add-or-update": "関係が更新されました",
+		"type-relation-delete": "関係が削除されました",
+		"type-relations-delete": "すべてのリレーションを削除",
+		"type-alarm-ack": "承認された",
+		"type-alarm-clear": "クリアされた",
 		"status-success": "成功",
 		"status-failure": "失敗",
 		"audit-log-details": "監査ログの詳細",
@@ -322,7 +322,7 @@
 	"common": {
 		"username": "ユーザー名",
 		"password": "パスワード",
-        "enter-username": "ユーザーネームを入力してください",
+		"enter-username": "ユーザーネームを入力してください",
 		"enter-password": "パスワードを入力する",
 		"enter-search": "検索を入力"
 	},
@@ -919,7 +919,6 @@
 		"modbus-register-count": "レジスタ数",
 		"modbus-register-count-range": "レジスタ数は正の値でなければなりません。",
 		"modbus-byte-order": "バイト順",
-
 		"sync": {
 			"status": "状態",
 			"sync": "同期",
@@ -927,7 +926,6 @@
 			"last-sync-time": "前回の同期時間",
 			"not-available": "利用不可"
 		},
-
 		"export-extensions-configuration": "エクステンション設定のエクスポート",
 		"import-extensions-configuration": "エクステンション設定のインポート",
 		"import-extensions": "拡張機能のインポート",
@@ -1417,7 +1415,7 @@
 		"action-source-required": "アクションソースが必要です。",
 		"action-name": "名",
 		"action-name-required": "アクション名は必須です。",
-        "action-name-not-unique": "同じ名前の別のアクションがすでに存在します。<br/>アクション名は、同じアクションソース内で一意である必要があります。",
+		"action-name-not-unique": "同じ名前の別のアクションがすでに存在します。<br/>アクション名は、同じアクションソース内で一意である必要があります。",
 		"action-icon": "アイコン",
 		"action-type": "タイプ",
 		"action-type-required": "アクションタイプが必要です。",
@@ -1427,7 +1425,7 @@
 		"delete-action-text": "'{{actionName}}'?"
 	},
 	"widget-type": {
-        "import": "インポートウィジェットタイプ",
+		"import": "インポートウィジェットタイプ",
 		"export": "ウィジェットのタイプをエクスポートする",
 		"export-failed-error": "{{error}}",
 		"create-new-widget-type": "新しいウィジェットタイプを作成する",
@@ -1451,6 +1449,7 @@
 	"language": {
 		"language": "言語",
 		"locales": {
+			"de_DE": "ドイツ語",
 			"en_US": "英語",
 			"ko_KR": "韓国語",
 			"it_IT": "イタリアの",
@@ -1461,4 +1460,4 @@
 			"tr_TR": "トルコ語"
 		}
 	}
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-ko_KR.json b/ui/src/app/locale/locale.constant-ko_KR.json
index 62c0054..4ddd9b5 100644
--- a/ui/src/app/locale/locale.constant-ko_KR.json
+++ b/ui/src/app/locale/locale.constant-ko_KR.json
@@ -85,7 +85,6 @@
         "enable-tls": "TLS 사용",
         "send-test-mail": "테스트 메일 보내기"
     },
-
     "alarm": {
         "alarm": "Alarm",
         "alarms": "Alarms",
@@ -831,7 +830,6 @@
         "modbus-register-count": "Register count",
         "modbus-register-count-range": "Register count should be a positive value.",
         "modbus-byte-order": "Byte order",
-
         "sync": {
             "status": "Status",
             "sync": "Sync",
@@ -839,7 +837,6 @@
             "last-sync-time": "Last sync time",
             "not-available": "Not available"
         },
-
         "export-extensions-configuration": "Export extensions configuration",
         "import-extensions-configuration": "Import extensions configuration",
         "import-extensions": "Import extensions",
@@ -1328,8 +1325,9 @@
     "language": {
         "language": "언어",
         "locales": {
+            "de_DE": "독일어",
             "en_US": "영어",
-			"fr_FR": "프랑스의",
+            "fr_FR": "프랑스의",
             "ko_KR": "한글",
             "zh_CN": "중국어",
             "ru_RU": "러시아어",
@@ -1339,4 +1337,4 @@
             "tr_TR": "터키어"
         }
     }
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json
index eb9996d..c087649 100644
--- a/ui/src/app/locale/locale.constant-ru_RU.json
+++ b/ui/src/app/locale/locale.constant-ru_RU.json
@@ -22,8 +22,9 @@
         "update": "Обновить",
         "remove": "Удалить",
         "search": "Поиск",
+        "clear-search": "Очистить",
         "assign": "Присвоить",
-        "unassign": "Отменить присвоение",
+        "unassign": "Отозвать",
         "share": "Поделиться",
         "make-private": "Закрыть для общего доступа",
         "apply": "Применить",
@@ -43,6 +44,8 @@
         "undo": "Откатить",
         "copy": "Копировать",
         "paste": "Вставить",
+        "copy-reference": "Копировать ссылку",
+        "paste-reference": "Вставить ссылку",
         "import": "Импортировать",
         "export": "Экспортировать",
         "share-via": "Поделиться в {{provider}}"
@@ -83,168 +86,179 @@
         "send-test-mail": "Отправить пробное письмо"
     },
     "alarm": {
-        "alarm": "Alarm",
-        "alarms": "Alarms",
-        "select-alarm": "Select alarm",
-        "no-alarms-matching": "No alarms matching '{{entity}}' were found.",
-        "alarm-required": "Alarm is required",
-        "alarm-status": "Alarm status",
+        "alarm": "Оповещение",
+        "alarms": "Оповещения",
+        "select-alarm": "Выбрать оповещение",
+        "no-alarms-matching": "Оповещения '{{entity}}' не найдены.",
+        "alarm-required": "Оповещение обязательно",
+        "alarm-status": "Статус оповещения",
         "search-status": {
-            "ANY": "Any",
-            "ACTIVE": "Active",
-            "CLEARED": "Cleared",
-            "ACK": "Acknowledged",
-            "UNACK": "Unacknowledged"
+            "ANY": "Все",
+            "ACTIVE": "Активные",
+            "CLEARED": "Сброшенные",
+            "ACK": "Подтвержденные",
+            "UNACK": "Неподтвержденные"
         },
         "display-status": {
-            "ACTIVE_UNACK": "Active Unacknowledged",
-            "ACTIVE_ACK": "Active Acknowledged",
-            "CLEARED_UNACK": "Cleared Unacknowledged",
-            "CLEARED_ACK": "Cleared Acknowledged"
+            "ACTIVE_UNACK": "Активные неподтвержденные",
+            "ACTIVE_ACK": "Активные подтвержденные",
+            "CLEARED_UNACK": "Сброшенные неподтвержденные",
+            "CLEARED_ACK": "Сброшенные подтвержденные"
         },
-        "no-alarms-prompt": "No alarms found",
-        "created-time": "Created time",
-        "type": "Type",
-        "severity": "Severity",
-        "originator": "Originator",
-        "originator-type": "Originator type",
-        "details": "Details",
-        "status": "Status",
-        "alarm-details": "Alarm details",
-        "start-time": "Start time",
-        "end-time": "End time",
-        "ack-time": "Acknowledged time",
-        "clear-time": "Cleared time",
-        "severity-critical": "Critical",
-        "severity-major": "Major",
-        "severity-minor": "Minor",
-        "severity-warning": "Warning",
-        "severity-indeterminate": "Indeterminate",
-        "acknowledge": "Acknowledge",
-        "clear": "Clear",
-        "search": "Search alarms",
-        "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selected",
-        "no-data": "No data to display",
-        "polling-interval": "Alarms polling interval (sec)",
-        "polling-interval-required": "Alarms polling interval is required.",
-        "min-polling-interval-message": "At least 1 sec polling interval is allowed.",
-        "aknowledge-alarms-title": "Acknowledge { count, plural, 1 {1 alarm} other {# alarms} }",
-        "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, 1 {1 alarm} other {# alarms} }?",
-        "clear-alarms-title": "Clear { count, plural, 1 {1 alarm} other {# alarms} }",
-        "clear-alarms-text": "Are you sure you want to clear { count, plural, 1 {1 alarm} other {# alarms} }?"
+        "no-alarms-prompt": "Оповещения отсутствуют",
+        "created-time": "Время создания",
+        "type": "Тип",
+        "severity": "Уровень",
+        "originator": "Инициатор",
+        "originator-type": "Тип инициатора",
+        "details": "Подробности",
+        "status": "Статус",
+        "alarm-details": "Подробности об оповещении",
+        "start-time": "Время начала",
+        "end-time": "Время окончания",
+        "ack-time": "Время подтверждения",
+        "clear-time": "Время сброса",
+        "severity-critical": "Критический",
+        "severity-major": "Основной",
+        "severity-minor": "Второстепенный",
+        "severity-warning": "Предупреждение",
+        "severity-indeterminate": "Неопределенный",
+        "acknowledge": "Подтвердить",
+        "clear": "Сбросить",
+        "search": "Поиск оповещений",
+        "selected-alarms": "Выбрано { count, plural, 1 {1 оповещение} few {# оповещения} other {# оповещений} }",
+        "no-data": "Нет данных для отображения",
+        "polling-interval": "Интервал опроса оповещений (сек)",
+        "polling-interval-required": "Интервал опроса оповещений обязателен.",
+        "min-polling-interval-message": "Минимальный интервал опроса оповещений 1 секунда.",
+        "aknowledge-alarms-title": "Подтвердить { count, plural, 1 {1 оповещение} other {# оповещений} }",
+        "aknowledge-alarms-text": "Вы точно хотите подтвердить { count, plural, 1 {1 оповещение} other {# оповещений} }?",
+        "aknowledge-alarm-title": "Подтвердить оповещение",
+        "aknowledge-alarm-text": "Вы точно хотите подтвердить оповещение?",
+        "clear-alarms-title": "Сбросить { count, plural, 1 {1 оповещение} other {# оповещений} }",
+        "clear-alarms-text": "Вы точно хотите сбросить { count, plural, 1 {1 оповещение} other {# оповещений} }?",
+        "clear-alarm-title": "Сбросить оповещение",
+        "clear-alarm-text": "Вы точно хотите сбросить оповещение?",
+        "alarm-status-filter": "Фильтр оповещений"
     },
     "alias": {
-        "add": "Add alias",
-        "edit": "Edit alias",
-        "name": "Alias name",
-        "name-required": "Alias name is required",
-        "duplicate-alias": "Alias with same name is already exists.",
-        "filter-type-single-entity": "Single entity",
-        "filter-type-entity-list": "Entity list",
-        "filter-type-entity-name": "Entity name",
-        "filter-type-state-entity": "Entity from dashboard state",
-        "filter-type-state-entity-description": "Entity taken from dashboard state parameters",
-        "filter-type-asset-type": "Asset type",
-        "filter-type-asset-type-description": "Assets of type '{{assetType}}'",
-        "filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'",
-        "filter-type-device-type": "Device type",
-        "filter-type-device-type-description": "Devices of type '{{deviceType}}'",
-        "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'",
-        "filter-type-relations-query": "Relations query",
-        "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
-        "filter-type-asset-search-query": "Asset search query",
-        "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
-        "filter-type-device-search-query": "Device search query",
-        "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
-        "entity-filter": "Entity filter",
-        "resolve-multiple": "Resolve as multiple entities",
-        "filter-type": "Filter type",
-        "filter-type-required": "Filter type is required.",
-        "entity-filter-no-entity-matched": "No entities matching specified filter were found.",
-        "no-entity-filter-specified": "No entity filter specified",
-        "root-state-entity": "Use dashboard state entity as root",
-        "root-entity": "Root entity",
-        "state-entity-parameter-name": "State entity parameter name",
-        "default-state-entity": "Default state entity",
-        "default-entity-parameter-name": "By default",
-        "max-relation-level": "Max relation level",
-        "unlimited-level": "Unlimited level",
-        "state-entity": "Dashboard state entity",
-        "all-entities": "All entities",
-        "any-relation": "any"
+        "add": "Добавить псевдоним",
+        "edit": "Редактировать псевдоним",
+        "name": "Псевдоним",
+        "name-required": "Псевдоним обязателен",
+        "duplicate-alias": "Такой псевдоним уже существует.",
+        "filter-type-single-entity": "Отдельный объект",
+        "filter-type-entity-list": "Список объектов",
+        "filter-type-entity-name": "Название объекта",
+        "filter-type-state-entity": "Объект, полученный из дашборда",
+        "filter-type-state-entity-description": "Объект, полученный из параметров дашборда",
+        "filter-type-asset-type": "Тип актива",
+        "filter-type-asset-type-description": "Активы типа '{{assetType}}'",
+        "filter-type-asset-type-and-name-description": "Активы типа '{{assetType}}' и названием, начинающимся с '{{prefix}}'",
+        "filter-type-device-type": "Тип устройства",
+        "filter-type-device-type-description": "Устройства типа '{{deviceType}}'",
+        "filter-type-device-type-and-name-description": "Устройства типа '{{deviceType}}' и названием, начинающимся с '{{prefix}}'",
+        "filter-type-entity-view-type": "Тип Представления Объекта",
+        "filter-type-entity-view-type-description": "Представления Объекта типа '{{entityView}}'",
+        "filter-type-entity-view-type-and-name-description": "Представления Объекта типа '{{entityView}}' и названием, начинающимся с '{{prefix}}'",
+        "filter-type-relations-query": "Запрос по типу отношений",
+        "filter-type-relations-query-description": "{{entities}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}",
+        "filter-type-asset-search-query": "Поисковый запрос по активам",
+        "filter-type-asset-search-query-description": "Активы типа {{assetTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}",
+        "filter-type-device-search-query": "Поисковый запрос по устройствам",
+        "filter-type-device-search-query-description": "Устройства типа {{deviceTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}",
+        "filter-type-entity-view-search-query": "Поисковый запрос по представлениям объектов",
+        "filter-type-entity-view-search-query-description": "Представления объектов типа {{entityViewTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}",
+        "entity-filter": "Фильтр объектов",
+        "resolve-multiple": "Принять как несколько объектов",
+        "filter-type": "Тип фильтра",
+        "filter-type-required": "Тип фильтра обязателен.",
+        "entity-filter-no-entity-matched": "Объекты, соответствующие фильтру, не найдены.",
+        "no-entity-filter-specified": "Не указан фильтр объектов",
+        "root-state-entity": "Использовать объект, полученный из дашборда, как корневой",
+        "root-entity": "Корневой объект",
+        "state-entity-parameter-name": "Название объекта состояния",
+        "default-state-entity": "Объект состояния по умолчанию",
+        "default-entity-parameter-name": "По умолчанию",
+        "max-relation-level": "Максимальная глубина отношений",
+        "unlimited-level": "Неограниченная глубина",
+        "state-entity": "Объект состояния дашборда",
+        "all-entities": "Все объекты",
+        "any-relation": "не указано"
     },
     "asset": {
-        "asset": "Asset",
-        "assets": "Assets",
-        "management": "Asset management",
-        "view-assets": "View Assets",
-        "add": "Add Asset",
-        "assign-to-customer": "Assign to customer",
-        "assign-asset-to-customer": "Assign Asset(s) To Customer",
-        "assign-asset-to-customer-text": "Please select the assets to assign to the customer",
-        "no-assets-text": "No assets found",
-        "assign-to-customer-text": "Please select the customer to assign the asset(s)",
-        "public": "Public",
-        "assignedToCustomer": "Assigned to customer",
-        "make-public": "Make asset public",
-        "make-private": "Make asset private",
-        "unassign-from-customer": "Unassign from customer",
-        "delete": "Delete asset",
-        "asset-public": "Asset is public",
-        "asset-type": "Asset type",
-        "asset-type-required": "Asset type is required.",
-        "select-asset-type": "Select asset type",
-        "enter-asset-type": "Enter asset type",
-        "any-asset": "Any asset",
-        "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.",
-        "asset-type-list-empty": "No asset types selected.",
-        "asset-types": "Asset types",
-        "name": "Name",
-        "name-required": "Name is required.",
-        "description": "Description",
-        "type": "Type",
-        "type-required": "Type is required.",
-        "details": "Details",
-        "events": "Events",
-        "add-asset-text": "Add new asset",
-        "asset-details": "Asset details",
-        "assign-assets": "Assign assets",
-        "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer",
-        "delete-assets": "Delete assets",
-        "unassign-assets": "Unassign assets",
-        "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer",
-        "assign-new-asset": "Assign new asset",
-        "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?",
-        "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.",
-        "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?",
-        "delete-assets-action-title": "Delete { count, plural, 1 {1 asset} other {# assets} }",
-        "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.",
-        "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?",
-        "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.",
-        "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?",
-        "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.",
-        "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?",
-        "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.",
-        "unassign-asset": "Unassign asset",
-        "unassign-assets-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?",
-        "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.",
-        "copyId": "Copy asset Id",
-        "idCopiedMessage": "Asset Id has been copied to clipboard",
-        "select-asset": "Select asset",
-        "no-assets-matching": "No assets matching '{{entity}}' were found.",
-        "asset-required": "Asset is required",
-        "name-starts-with": "Asset name starts with"
+        "asset": "Актив",
+        "assets": "Активы",
+        "management": "Управление активами",
+        "view-assets": "Просмотреть активы",
+        "add": "Добавить актив",
+        "assign-to-customer": "Присвоить клиенту",
+        "assign-asset-to-customer": "Присвоить актив(ы) клиенту",
+        "assign-asset-to-customer-text": "Пожалуйста, выберите активы, которые нужно присвоить объекту",
+        "no-assets-text": "Активы не найдены",
+        "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить актив(ы)",
+        "public": "Общедоступные",
+        "assignedToCustomer": "Присвоить клиенту",
+        "make-public": "Открыть общий доступ к активу",
+        "make-private": "Закрыть общий доступ к активу",
+        "unassign-from-customer": "Отозвать у клиента",
+        "delete": "Удалить актив",
+        "asset-public": "Актив общедоступный",
+        "asset-type": "Тип актива",
+        "asset-type-required": "Тип актива обязателен.",
+        "select-asset-type": "Выберите тип актива",
+        "enter-asset-type": "Введите тип актива",
+        "any-asset": "Любой актив",
+        "no-asset-types-matching": "Активы типа '{{entitySubtype}}' не найдены.",
+        "asset-type-list-empty": "Типы активов не выбраны.",
+        "asset-types": "Типы активов",
+        "name": "Название",
+        "name-required": "Название обязательно.",
+        "description": "Описание",
+        "type": "Тип",
+        "type-required": "Тип обязателен.",
+        "details": "Подробности",
+        "events": "События",
+        "add-asset-text": "Добавить новый актив",
+        "asset-details": "Подробности об активе",
+        "assign-assets": "Присвоить активы",
+        "assign-assets-text": "Присвоить { count, plural, 1 {1 актив} few {# актива} other {# активов} } клиенту",
+        "delete-assets": "Удалить активы",
+        "unassign-assets": "Отозвать активы",
+        "unassign-assets-action-title": "Отозвать { count, plural, 1 {1 актив} few {# актива} other {# активов} } у клиента",
+        "assign-new-asset": "Присвоить новый актив",
+        "delete-asset-title": "Вы точно хотите удалить '{{assetName}}'?",
+        "delete-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные будут безвозвратно удалены.",
+        "delete-assets-title": "Вы точно хотите удалить { count, plural, 1 {1 актив} few {# актива} other {# активов} }",
+        "delete-assets-action-title": "Удалить { count, plural, 1 {1 актив} few {# актива} other {# активов} }",
+        "delete-assets-text": "Внимание, после подтверждения выбранные активы и все связанные с ними данные будут безвозвратно удалены.",
+        "make-public-asset-title": "Вы точно хотите открыть общий доступ к активу '{{assetName}}'?",
+        "make-public-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные станут общедоступными.",
+        "make-private-asset-title": "Вы точно хотите закрыть общий доступ к активу '{{assetName}}'?",
+        "make-private-asset-text": "После подтверждения актив и все связанные с ним данные будут закрыты для общего доступа",
+        "unassign-asset-title": "Вы точно хотите отозвать актив '{{assetName}}'?",
+        "unassign-asset-text": "После подтверждения актив будут отозван, и клиент потеряет к нему доступ.",
+        "unassign-asset": "Отозвать актив",
+        "unassign-assets-title": "Вы точно хотите отозвать { count, plural, 1 {1 актив} few {# актива} other {# активов} }?",
+        "unassign-assets-text": "После подтверждения активы будут отозваны, и клиент потеряет к ним доступ.",
+        "copyId": "Копировать ИД актива",
+        "idCopiedMessage": "ИД актива скопировано в буфер обмена",
+        "select-asset": "Выбрать активы",
+        "no-assets-matching": "Активы, соответствующие '{{entity}}', не найдены.",
+        "asset-required": "Актив обязателен",
+        "name-starts-with": "Название актива, начинающееся с"
     },
     "attribute": {
         "attributes": "Атрибуты",
         "latest-telemetry": "Последняя телеметрия",
-        "attributes-scope": "Контекст атрибутов устройства",
+        "attributes-scope": "Контекст атрибутов объекта",
         "scope-latest-telemetry": "Последняя телеметрия",
         "scope-client": "Клиентские атрибуты",
         "scope-server": "Серверные атрибуты",
         "scope-shared": "Общие атрибуты",
         "add": "Добавить атрибут",
         "key": "Ключ",
+        "last-update-time": "Последнее обновление",
         "key-required": "Ключ атрибута обязателен.",
         "value": "Значение",
         "value-required": "Значение атрибута обязательно.",
@@ -262,36 +276,41 @@
         "selected-telemetry": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, 1 {1 параметр} few {# параметра} other {# параметров} } телеметрии"
     },
     "audit-log": {
-        "audit": "Audit",
-        "audit-logs": "Audit Logs",
-        "timestamp": "Timestamp",
-        "entity-type": "Entity Type",
-        "entity-name": "Entity Name",
-        "user": "User",
-        "type": "Type",
-        "status": "Status",
-        "details": "Details",
-        "type-added": "Added",
-        "type-deleted": "Deleted",
-        "type-updated": "Updated",
-        "type-attributes-updated": "Attributes updated",
-        "type-attributes-deleted": "Attributes deleted",
-        "type-rpc-call": "RPC call",
-        "type-credentials-updated": "Credentials updated",
-        "type-assigned-to-customer": "Assigned to Customer",
-        "type-unassigned-from-customer": "Unassigned from Customer",
-        "type-activated": "Activated",
-        "type-suspended": "Suspended",
-        "type-credentials-read": "Credentials read",
-        "type-attributes-read": "Attributes read",
-        "status-success": "Success",
-        "status-failure": "Failure",
-        "audit-log-details": "Audit log details",
-        "no-audit-logs-prompt": "No logs found",
-        "action-data": "Action data",
-        "failure-details": "Failure details",
-        "search": "Search audit logs",
-        "clear-search": "Clear search"
+        "audit": "Аудит",
+        "audit-logs": "Логи аудита",
+        "timestamp": "Время",
+        "entity-type": "Тип объекта",
+        "entity-name": "Название объекта",
+        "user": "Пользователь",
+        "type": "Тип",
+        "status": "Статус",
+        "details": "Подробности",
+        "type-added": "Добавленный",
+        "type-deleted": "Удаленный",
+        "type-updated": "Обновленный",
+        "type-attributes-updated": "Обновлены атрибуты",
+        "type-attributes-deleted": "Удалены атрибуты",
+        "type-rpc-call": "RPC вызов",
+        "type-credentials-updated": "Обновлены учетные данные",
+        "type-assigned-to-customer": "Присвоен клиенту",
+        "type-unassigned-from-customer": "Отозван у клиента",
+        "type-activated": "Активирован",
+        "type-suspended": "Приостановлен",
+        "type-credentials-read": "Чтение учетные данных",
+        "type-attributes-read": "Чтение атрибутов",
+        "type-relation-add-or-update": "Обновлены отношения",
+        "type-relation-delete": "Удалены отношения",
+        "type-relations-delete": "Удалены все отношения",
+        "type-alarm-ack": "Подтвержден",
+        "type-alarm-clear": "Сброшен",
+        "status-success": "Успех",
+        "status-failure": "Сбой",
+        "audit-log-details": "Подробности аудит лога",
+        "no-audit-logs-prompt": "Логи не найдены",
+        "action-data": "Данные действия",
+        "failure-details": "Подробности сбоя",
+        "search": "Поиск аудит логов",
+        "clear-search": "Очистить поиск"
     },
     "confirm-on-exit": {
         "message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?",
@@ -319,17 +338,22 @@
     },
     "content-type": {
         "json": "Json",
-        "text": "Text",
-        "binary": "Binary (Base64)"
+        "text": "Текстовый",
+        "binary": "Бинарный (Base64)"
     },
     "customer": {
+        "customer": "Клиент",
         "customers": "Клиенты",
         "management": "Управление клиентами",
-        "dashboard": "Дашборд клиентов",
-        "dashboards": "Дашборды клиентов",
+        "dashboard": "Дашборд клиента",
+        "dashboards": "Дашборды клиента",
         "devices": "Устройства клиента",
+        "entity-views": "Представления объектов клиента",
+        "assets": "Активы клиента",
         "public-dashboards": "Общедоступные дашборды",
         "public-devices": "Общедоступные устройства",
+        "public-assets": "Общедоступные активы",
+        "public-entity-views": "Общедоступные представления объектов",
         "add": "Добавить клиента",
         "delete": "Удалить клиента",
         "manage-customer-users": "Управление пользователями клиента",
@@ -337,31 +361,33 @@
         "manage-customer-dashboards": "Управление дашбордами клиента",
         "manage-public-devices": "Управление общедоступными устройствами",
         "manage-public-dashboards": "Управление общедоступными дашбордами",
+        "manage-customer-assets": "Управление активами клиента",
+        "manage-public-assets": "Управление общедоступными активами",
         "add-customer-text": "Добавить нового клиента",
         "no-customers-text": "Клиенты не найдены",
         "customer-details": "Подробности о клиенте",
         "delete-customer-title": "Вы точно хотите удалить клиента '{{customerTitle}}'?",
-        "delete-customer-text": "Внимание, после подтверждения клиент и вся связанная с ним информация будут безвозвратно утеряны.",
-        "delete-customers-title": "Вы точно хотите удалить { count, plural, one {1 клиента} other {# клиентов} }?",
-        "delete-customers-action-title": "Удалить { count, plural, one {1 клиента} other {# клиентов} } }",
-        "delete-customers-text": "Внимание, после подтверждения клиенты и вся связанная с ними информация будут безвозвратно утеряны.",
+        "delete-customer-text": "Внимание, после подтверждения клиент и все связанные с ним данные будут безвозвратно удалены.",
+        "delete-customers-title": "Вы точно хотите удалить { count, plural, 1 {1 клиент} few {# клиента} other {# клиентов} }?",
+        "delete-customers-action-title": "Удалить { count, plural, 1 {1 клиент} few {# клиента} other {# клиентов} }",
+        "delete-customers-text": "Внимание, после подтверждения выбранные клиенты и все связанные с ними данные будут безвозвратно удалены.",
         "manage-users": "Управление пользователями",
-        "manage-assets": "Manage assets",
+        "manage-assets": "Управление активами",
         "manage-devices": "Управление устройствами",
         "manage-dashboards": "Управление дашбордами",
         "title": "Имя",
         "title-required": "Название обязательно.",
         "description": "Описание",
-        "details": "Details",
-        "events": "Events",
-        "copyId": "Copy customer Id",
-        "idCopiedMessage": "Customer Id has been copied to clipboard",
-        "select-customer": "Select customer",
-        "no-customers-matching": "No customers matching '{{entity}}' were found.",
-        "customer-required": "Customer is required",
-        "select-default-customer": "Select default customer",
-        "default-customer": "Default customer",
-        "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level"
+        "details": "Подробности",
+        "events": "События",
+        "copyId": "Копировать ИД клиента",
+        "idCopiedMessage": "ИД клиента скопирован в буфер обмена",
+        "select-customer": "Выбрать клиента",
+        "no-customers-matching": "Клиенты, соответствующие '{{entity}}', не найдены.",
+        "customer-required": "Клиент обязателен",
+        "select-default-customer": "Выбрать клиента по умолчанию",
+        "default-customer": "Клиент по умолчанию",
+        "default-customer-required": "Клиент по умолчанию обязателен для отладки дашборда на уровне на уровне Владельца"
     },
     "datetime": {
         "date-from": "Дата с",
@@ -379,9 +405,15 @@
         "assign-dashboard-to-customer-text": "Пожалуйста, выберите дашборды, которые нужно прикрепить к клиенту",
         "assign-to-customer-text": "Пожалуйста, выберите клиента, к которому нужно прикрепить дашборд(ы)",
         "assign-to-customer": "Прикрепить к клиенту",
-        "unassign-from-customer": "Открепить от клиента",
+        "unassign-from-customer": "Отозвать у клиента",
         "make-public": "Открыть дашборд для общего доступа",
         "make-private": "Закрыть дашборд для общего доступа",
+        "manage-assigned-customers": "Управление назначенными клиентами",
+        "assigned-customers": "Назначенные клиенты",
+        "assign-to-customers": "Присвоить дашборд(ы) клиенту",
+        "assign-to-customers-text": "Пожалуйста, выбери клиентов, которым нужно присвоить дашборд(ы)",
+        "unassign-from-customers": "Отозвать дашборд(ы) у клиентов",
+        "unassign-from-customers-text": "Пожалуйста, выберите клиентов, у которых нужно отозвать дашборд(ы)",
         "no-dashboards-text": "Дашборды не найдены",
         "no-widgets": "Виджеты не сконфигурированы",
         "add-widget": "Добавить новый виджет",
@@ -396,19 +428,20 @@
         "add-dashboard-text": "Добавить новый дашборд",
         "assign-dashboards": "Прикрепить дашборды",
         "assign-new-dashboard": "Прикрепить новый дашборд",
-        "assign-dashboards-text": "Прикрепить { count, plural, 1 {1 дашборд} other {# дашборда} } к клиенту",
+        "assign-dashboards-text": "Прикрепить { count, plural, 1 {1 дашборд} few {# дашборда} other {# дашбордов} } к клиенту",
+        "unassign-dashboards-action-text": "Отозвать { count, plural, 1 {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента",
         "delete-dashboards": "Удалить дашборды",
-        "unassign-dashboards": "Открепить дашборды",
-        "unassign-dashboards-action-title": "Открепить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} } от клиента",
+        "unassign-dashboards": "Отозвать дашборды",
+        "unassign-dashboards-action-title": "Отозвать { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента",
         "delete-dashboard-title": "Вы точно хотите удалить дашборд '{{dashboardTitle}}'?",
         "delete-dashboard-text": "Внимание, после подтверждения дашборд и все связанные с ним данные будут безвозвратно утеряны.",
         "delete-dashboards-title": "Вы точно хотите удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?",
         "delete-dashboards-action-title": "Удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }",
         "delete-dashboards-text": "Внимание, после подтверждения дашборды и все связанные с ними данные будут безвозвратно утеряны.",
-        "unassign-dashboard-title": "Вы точно хотите открепить дашборд '{{dashboardTitle}}'?",
+        "unassign-dashboard-title": "Вы точно хотите отозвать дашборд '{{dashboardTitle}}'?",
         "unassign-dashboard-text": "После подтверждения дашборд не будет доступен клиенту.",
-        "unassign-dashboard": "Открепить дашборд",
-        "unassign-dashboards-title": "Вы точно хотите открепить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?",
+        "unassign-dashboard": "Отозвать дашборд",
+        "unassign-dashboards-title": "Вы точно хотите отозвать { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?",
         "unassign-dashboards-text": "После подтверждения выбранные дашборды не будут доступны клиенту.",
         "public-dashboard-title": "Теперь дашборд общедоступный",
         "public-dashboard-text": "Теперь ваш дашборд <b>{{dashboardTitle}}</b> доступен всем по <a href='{{publicLink}}' target='_blank'>ссылке</a>:",
@@ -445,9 +478,17 @@
         "vertical-margin-required": "Величина вертикального отступа обязательна.",
         "min-vertical-margin-message": "Минимальная величина вертикального отступа - 0.",
         "max-vertical-margin-message": "Максимальная величина вертикального отступа - 50.",
+        "autofill-height": "Автозаполнение по высоте",
+        "mobile-layout": "Настройки мобильного режима",
+        "mobile-row-height": "Высота строки в мобильном режиме, px",
+        "mobile-row-height-required": "Высота строки в мобильном режиме обязательна.",
+        "min-mobile-row-height-message": "Минимальная высота строки в мобильном режиме составляет 5 px.",
+        "max-mobile-row-height-message": "Максимальная высота строки в мобильном режиме составляет 200 px.",
         "display-title": "Показать название дашборда",
+        "toolbar-always-open": "Отображать панель инструментов",
         "title-color": "Цвет названия",
-        "display-device-selection": "Показать выборку устройств",
+        "display-dashboards-selection": "Отображать выборку дашбордов",
+        "display-entities-selection": "Отображать выбору объектов",
         "display-dashboard-timewindow": "Показать временное окно",
         "display-dashboard-export": "Показать экспорт",
         "import": "Импортировать дашборд",
@@ -456,66 +497,74 @@
         "create-new-dashboard": "Создать новый дашборд",
         "dashboard-file": "Файл дашборда",
         "invalid-dashboard-file-error": "Не удалось импортировать дашборд: неизвестная схема данных дашборда.",
-        "dashboard-import-missing-aliases-title": "Конфигурировать псевдонимы импортированного дашборда",
+        "dashboard-import-missing-aliases-title": "Настроить псевдонимы импортированного дашборда",
         "create-new-widget": "Создать новый виджет",
-        "import-widget": "Импортировать новый виджет",
-        "widget-file": "Файл виджета",
-        "invalid-widget-file-error": "Не удалось импортировать виджет: неизвестная схема данных виджета.",
-        "widget-import-missing-aliases-title": "Конфигурировать псевдонимы импортированного виджета",
-        "open-toolbar": "Открыть панель инструментов",
+        "import-widget": "Импортировать виджет",
+        "widget-file": "Виджет-файл",
+        "invalid-widget-file-error": "Не удалось импортировать виджет: неправильный формат данных.",
+        "widget-import-missing-aliases-title": "Настроить псевдонимы, которые использует импортированный виджет",
+        "open-toolbar": "Открыть панель инструментов дашборда",
         "close-toolbar": "Закрыть панель инструментов",
-        "configuration-error": "Ошибка конфигурирования",
-        "alias-resolution-error-title": "Ошибка конфигурирования псевдонимов дашборда",
-        "invalid-aliases-config": "Не удалось найти устройства, соответствующие фильтру псевдонимов.<br/>Пожалуйста, свяжитесь с администратором для устранения этой проблемы.",
+        "configuration-error": "Ошибка в настройках",
+        "alias-resolution-error-title": "Ошибка в настройках псевдонимов дашборда",
+        "invalid-aliases-config": "Не удалось найти устройство, соответствующее фильтру псевдонимов.<br/>Пожалуйста, обратитесь к администратору для устранения неполадки.",
         "select-devices": "Выберите устройства",
-        "assignedToCustomer": "Прикреплен к клиенту",
-        "public": "Общедоступный",
-        "public-link": "Общедоступная ссылка",
-        "copy-public-link": "Скопировать общедоступную ссылку",
-        "public-link-copied-message": "Общедоступная ссылка на дашборд скопирована в буфер обмена",
-        "manage-states": "Manage dashboard states",
-        "states": "Dashboard states",
-        "search-states": "Search dashboard states",
-        "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } selected",
-        "edit-state": "Edit dashboard state",
-        "delete-state": "Delete dashboard state",
-        "add-state": "Add dashboard state",
-        "state": "Dashboard state",
-        "state-name": "Name",
-        "state-name-required": "Dashboard state name is required.",
-        "state-id": "State Id",
-        "state-id-required": "Dashboard state id is required.",
-        "state-id-exists": "Dashboard state with the same id is already exists.",
-        "is-root-state": "Root state",
-        "delete-state-title": "Delete dashboard state",
-        "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?",
-        "show-details": "Show details",
-        "hide-details": "Hide details",
-        "select-state": "Select target state",
-        "state-controller": "State controller"
+        "assignedToCustomer": "Присвоенные клиенту",
+        "assignedToCustomers": "Присвоенные клиентам",
+        "public": "Публичный",
+        "public-link": "Публичная ссылка",
+        "copy-public-link": "Копировать публичную ссылку",
+        "public-link-copied-message": "Публичная ссылка на дашборд скопирована в буфер обмена.",
+        "manage-states": "Управление состоянием дашборда",
+        "states": "Состояния дашборда",
+        "search-states": "Поиск состояния дашборда",
+        "selected-states": "Выбрано { count, plural, 1 {1 состояние} few {# состояния} other {# состояний} } дашборда",
+        "edit-state": "Изменить состояние дашборда",
+        "delete-state": "Удалить состояние дашборда",
+        "add-state": "Добавить состояние дашборда",
+        "state": "Состояние дашборда",
+        "state-name": "Название",
+        "state-name-required": "Название состояния дашборда обязательно.",
+        "state-id": "ИД состояния",
+        "state-id-required": "ИД состояния дашборда обязателен.",
+        "state-id-exists": "Состояния дашборда с таким именем уже существует.",
+        "is-root-state": "Корневое состояние",
+        "delete-state-title": "Удалить состояние дашборда",
+        "delete-state-text": "Вы точно хотите удалить состояние дашборда '{{stateName}}'?",
+        "show-details": "Показать подробности",
+        "hide-details": "Скрыть подробности",
+        "select-state": "Выбрать состояние",
+        "state-controller": "Контроллер состояния"
     },
     "datakey": {
         "settings": "Настройки",
         "advanced": "Дополнительно",
         "label": "Метка",
         "color": "Цвет",
-        "units": "Special symbol to show next to value",
-        "decimals": "Number of digits after floating point",
+        "units": "Укажите символы, которые нужно указывать после значения",
+        "decimals": "Число знаков после запятой",
         "data-generation-func": "Функция генерации данных",
         "use-data-post-processing-func": "Использовать функцию пост-обработки данных",
         "configuration": "Конфигурация ключа данных",
-        "timeseries": "Выборка по времени",
+        "timeseries": "Телеметрия",
         "attributes": "Атрибуты",
-        "timeseries-required": "Выборка по времени обязательна.",
-        "timeseries-or-attributes-required": "Выборка по времени/атрибуты обязательны.",
-        "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }",
-        "alarm-fields-required": "Alarm fields are required.",
+        "alarm": "Параметры оповещения",
+        "timeseries-required": "Телеметрия объекта обязательна.",
+        "timeseries-or-attributes-required": "Телеметрия/атрибуты обязательны.",
+        "maximum-timeseries-or-attributes": "Максимальное количество параметров телеметрии или атрибутов равно {{count}}",
+        "alarm-fields-required": "Параметры оповещения обязательны.",
         "function-types": "Тип функции",
         "function-types-required": "Тип функции обязателен.",
-        "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }"
+        "maximum-function-types": "Максимальное количество типов функции равно {{count}}",
+        "time-description": "время текущего значения;",
+        "value-description": "текущее значение;",
+        "prev-value-description": "результат предыдущего вызова функции;",
+        "time-prev-description": "время предыдущего значения;",
+        "prev-orig-value-description": "исходное предыдущее значение;"
     },
     "datasource": {
         "type": "Тип источника данных",
+        "name": "Название",
         "add-datasource-prompt": "Пожалуйста, добавьте источник данных"
     },
     "details": {
@@ -537,7 +586,7 @@
         "create-new-alias": "Создать новый!",
         "create-new-key": "Создать новый!",
         "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.<br>В рамках дашборда псевдонимы устройств должны быть уникальными.",
-        "configure-alias": "Конфигурировать '{{alias}}' псевдоним",
+        "configure-alias": "Настроить '{{alias}}' псевдоним",
         "no-devices-matching": "Устройство '{{entity}}' не найдено.",
         "alias": "Псевдоним",
         "alias-required": "Псевдоним устройства обязателен.",
@@ -565,9 +614,9 @@
         "assign-devices": "Присвоить устройство",
         "assign-devices-text": "Присвоить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } клиенту",
         "delete-devices": "Удалить устройства",
-        "unassign-from-customer": "Отменить присвоение клиенту",
-        "unassign-devices": "Отменить присвоение устройств",
-        "unassign-devices-action-title": "Отменить присвоение { count, plural, one {1 устройства} few {# устройств} other {# устройств} } клиенту",
+        "unassign-from-customer": "Отозвать у клиенту",
+        "unassign-devices": "Отозвать устройства",
+        "unassign-devices-action-title": "Отозвать у клиента { count, plural, one {1 устройство} few {# устройства} other {# устройств} }",
         "assign-new-device": "Присвоить новое устройство",
         "make-public-device-title": "Вы точно хотите открыть общий доступ к устройству '{{deviceName}}'?",
         "make-public-device-text": "После подтверждения устройство и все связанные с ним данные будут общедоступными.",
@@ -579,10 +628,10 @@
         "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?",
         "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }",
         "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..",
-        "unassign-device-title": "Вы точно хотите отменить присвоение устройства '{{deviceName}}'?",
+        "unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?",
         "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.",
-        "unassign-device": "Отменить присвоение устройства",
-        "unassign-devices-title": "Вы точно хотите отменить присвоение { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }?",
+        "unassign-device": "Отозвать устройство",
+        "unassign-devices-title": "Вы точно хотите отозвать { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?",
         "unassign-devices-text": "После подтверждения выбранные устройства будут недоступны клиенту.",
         "device-credentials": "Учетные данные устройства",
         "credentials-type": "Тип учетных данных",
@@ -593,6 +642,14 @@
         "rsa-key-required": "Открытый ключ RSA обязателен.",
         "secret": "Секрет",
         "secret-required": "Секрет обязателен.",
+        "device-type": "Тип устройства",
+        "device-type-required": "Тип устройства обязатеен.",
+        "select-device-type": "Выберите тип устройства",
+        "enter-device-type": "Введите тип устройства",
+        "any-device": "Любое устройство",
+        "no-device-types-matching": "Тип устройства, соответствующий '{{entitySubtype}}', не найден.",
+        "device-type-list-empty": "Не выбран тип устройства.",
+        "device-types": "Типы устройств",
         "name": "Название",
         "name-required": "Название обязательно.",
         "description": "Описание",
@@ -607,7 +664,8 @@
         "unable-delete-device-alias-text": "Не удалось удалить псевдоним '{{deviceAlias}}' устройства, т.к. он используется следующими виджетами:<br/>{{widgetsList}}",
         "is-gateway": "Гейтвей",
         "public": "Общедоступный",
-        "device-public": "Устройство общедоступно"
+        "device-public": "Устройство общедоступно",
+        "select-device": "Выбрать устройство"
     },
     "dialog": {
         "close": "Закрыть диалог"
@@ -618,100 +676,200 @@
         "unknown-error": "Неизвестная ошибка"
     },
     "entity": {
-        "entity": "Entity",
-        "entities": "Entities",
-        "aliases": "Entity aliases",
-        "entity-alias": "Entity alias",
-        "unable-delete-entity-alias-title": "Unable to delete entity alias",
-        "unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
-        "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity aliases must be unique whithin the dashboard.",
-        "missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.",
-        "configure-alias": "Configure '{{alias}}' alias",
-        "alias": "Alias",
-        "alias-required": "Entity alias is required.",
-        "remove-alias": "Remove entity alias",
-        "add-alias": "Add entity alias",
-        "entity-list": "Entity list",
-        "entity-type": "Entity type",
-        "entity-types": "Entity types",
-        "entity-type-list": "Entity type list",
-        "any-entity": "Any entity",
-        "enter-entity-type": "Enter entity type",
-        "no-entities-matching": "No entities matching '{{entity}}' were found.",
-        "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.",
-        "name-starts-with": "Name starts with",
-        "use-entity-name-filter": "Use filter",
-        "entity-list-empty": "No entities selected.",
-        "entity-type-list-empty": "No entity types selected.",
-        "entity-name-filter-required": "Entity name filter is required.",
-        "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
-        "all-subtypes": "All",
-        "select-entities": "Select entities",
-        "no-aliases-found": "No aliases found.",
-        "no-alias-matching": "'{{alias}}' not found.",
-        "create-new-alias": "Create a new one!",
-        "key": "Key",
-        "key-name": "Key name",
-        "no-keys-found": "No keys found.",
-        "no-key-matching": "'{{key}}' not found.",
-        "create-new-key": "Create a new one!",
-        "type": "Type",
-        "type-required": "Entity type is required.",
-        "type-device": "Device",
-        "type-devices": "Devices",
-        "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }",
-        "device-name-starts-with": "Devices whose names start with '{{prefix}}'",
-        "type-asset": "Asset",
-        "type-assets": "Assets",
-        "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }",
-        "asset-name-starts-with": "Assets whose names start with '{{prefix}}'",
-        "type-rule": "Rule",
-        "type-rules": "Rules",
-        "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }",
-        "rule-name-starts-with": "Rules whose names start with '{{prefix}}'",
-        "type-plugin": "Plugin",
-        "type-plugins": "Plugins",
-        "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }",
-        "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'",
-        "type-tenant": "Tenant",
-        "type-tenants": "Tenants",
-        "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }",
-        "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'",
-        "type-customer": "Customer",
-        "type-customers": "Customers",
-        "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }",
-        "customer-name-starts-with": "Customers whose names start with '{{prefix}}'",
-        "type-user": "User",
-        "type-users": "Users",
-        "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }",
-        "user-name-starts-with": "Users whose names start with '{{prefix}}'",
-        "type-dashboard": "Dashboard",
-        "type-dashboards": "Dashboards",
-        "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }",
-        "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'",
-        "type-alarm": "Alarm",
-        "type-alarms": "Alarms",
-        "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }",
-        "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
-        "type-rulechain": "Rule chain",
-        "type-rulechains": "Rule chains",
-        "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }",
-        "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
-        "type-current-customer": "Current Customer",
-        "search": "Search entities",
-        "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected",
-        "entity-name": "Entity name",
-        "details": "Entity details",
-        "no-entities-prompt": "No entities found",
-        "no-data": "No data to display"
+        "entity": "Объект",
+        "entities": "Объекты",
+        "aliases": "Псевдонимы объекта",
+        "entity-alias": "Псевдоним объекта",
+        "unable-delete-entity-alias-title": "Не удалось удалить псевдоним объекта",
+        "unable-delete-entity-alias-text": "Псевдоним объекта '{{entityAlias}}' не может быть удален, так как используется следующим(и) виджетом(ами):<br/>{{widgetsList}}",
+        "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.<br>В рамках одного дашборда псевдонимы объектов должны быть уникальными.",
+        "missing-entity-filter-error": "Отсутствует фильтр для псевдонима '{{alias}}'.",
+        "configure-alias": "Настроить псевдоним '{{alias}}'",
+        "alias": "Псевдоним",
+        "alias-required": "Псевдоним объекта обязателен.",
+        "remove-alias": "Убрать псевдоним объекта",
+        "add-alias": "Добавить псевдоним объекта",
+        "entity-list": "Список объектов",
+        "entity-type": "Тип объекта",
+        "entity-types": "Типы объекта",
+        "entity-type-list": "Список типов объекта",
+        "any-entity": "Любой объект",
+        "enter-entity-type": "Введите тип объекта",
+        "no-entities-matching": "Объекты, соответствующие '{{entity}}', не найдены.",
+        "no-entity-types-matching": "Типы объектов, соответствующие '{{entityType}}', не найдены.",
+        "name-starts-with": "Название, начинающееся с",
+        "use-entity-name-filter": "Использовать фильтр",
+        "entity-list-empty": "Не выбраны объекты.",
+        "entity-type-list-empty": "Не выбраны типы объекта.",
+        "entity-name-filter-required": "Фильтр по названию объекта обязателен.",
+        "entity-name-filter-no-entity-matched": "Объекты, чье название начинается с '{{entity}}', не найдены.",
+        "all-subtypes": "Все",
+        "select-entities": "Выберите объекты",
+        "no-aliases-found": "Псевдонимы не найдены.",
+        "no-alias-matching": "Псевдоним '{{alias}}' не найден.",
+        "create-new-alias": "Создать новый!",
+        "key": "Ключ",
+        "key-name": "Название ключа",
+        "no-keys-found": "Ключ не найден.",
+        "no-key-matching": "Ключ '{{key}}' не найден.",
+        "create-new-key": "Создать новый!",
+        "type": "Тип",
+        "type-required": "Тип объекта обязателен.",
+        "type-device": "Устройство",
+        "type-devices": "Устройства",
+        "list-of-devices": "{ count, plural, 1 {Одно устройство} other {Список из # устройств} }",
+        "device-name-starts-with": "Устройства, чьи название начинается с '{{prefix}}'",
+        "type-asset": "Актив",
+        "type-assets": "Активы",
+        "list-of-assets": "{ count, plural, 1 {Один актив} other {Список из # активов} }",
+        "asset-name-starts-with": "Активы, чьи название начинается с '{{prefix}}'",
+        "type-entity-view": "Представление Объекта",
+        "type-entity-views": "Представления Объекта",
+        "list-of-entity-views": "{ count, plural, 1 {Одно представление объекта} other {Список из # представлений объекта} }",
+        "entity-view-name-starts-with": "Представления Объекта, чьи название начинается с '{{prefix}}'",
+        "type-rule": "Правило",
+        "type-rules": "Правила",
+        "list-of-rules": "{ count, plural, 1 {Одно правило} other {Список из # правил} }",
+        "rule-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'",
+        "type-plugin": "Плагин",
+        "type-plugins": "Плагины",
+        "list-of-plugins": "{ count, plural, 1 {Один плагин} other {Список из # плагинов} }",
+        "plugin-name-starts-with": "Плагины, чьи имена начинаются с '{{prefix}}'",
+        "type-tenant": "Владелец",
+        "type-tenants": "Владельцы",
+        "list-of-tenants": "{ count, plural, 1 {Один владелец} other {Список из # владельцев} }",
+        "tenant-name-starts-with": "Владельцы, чьи имена начинаются с '{{prefix}}'",
+        "type-customer": "Клиент",
+        "type-customers": "Клиенты",
+        "list-of-customers": "{ count, plural, 1 {Один клиент} other {Список из # клиентов} }",
+        "customer-name-starts-with": "Клиенты, чьи имена начинаются с '{{prefix}}'",
+        "type-user": "Пользователь",
+        "type-users": "Пользователи",
+        "list-of-users": "{ count, plural, 1 {Один пользователь} other {Список из # пользователей} }",
+        "user-name-starts-with": "Пользователи, чьи имена начинаются с '{{prefix}}'",
+        "type-dashboard": "Дашборд",
+        "type-dashboards": "Дашборды",
+        "list-of-dashboards": "{ count, plural, 1 {Один дашборд} other {Список из # дашбордов} }",
+        "dashboard-name-starts-with": "Дашборды, чьи названия начинаются с '{{prefix}}'",
+        "type-alarm": "Оповещение",
+        "type-alarms": "Оповещения",
+        "list-of-alarms": "{ count, plural, 1 {Одно оповещение} other {Список из # оповещений} }",
+        "alarm-name-starts-with": "Оповещения, чьи названия начинаются с '{{prefix}}'",
+        "type-rulechain": "Цепочка правил",
+        "type-rulechains": "Цепочки правил",
+        "list-of-rulechains": "{ count, plural, 1 {Одна цепочка правил} other {Список из # цепочек правил} }",
+        "rulechain-name-starts-with": "Цепочки правил, чьи названия начинаются с '{{prefix}}'",
+        "type-rulenode": "Правило",
+        "type-rulenodes": "Правила",
+        "list-of-rulenodes": "{ count, plural, 1 {Одно правило} other {Список из # правил} }",
+        "rulenode-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'",
+        "type-current-customer": "Текущий клиент",
+        "search": "Поиск объектов",
+        "selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }",
+        "entity-name": "Название объекта",
+        "details": "Подробности об объекте",
+        "no-entities-prompt": "Объекты не найдены",
+        "no-data": "Нет данных для отображения",
+        "columns-to-display": "Отобразить следующие колонки"
+    },
+    "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 представление объекта} few {# представления объектов} other {# представлений объектов} }",
+        "delete-entity-views": "Удалить представления объектов",
+        "unassign-from-customer": "Отозвать у клиента",
+        "unassign-entity-views": "Отозвать представления объектов",
+        "unassign-entity-views-action-title": "Отозвать у клиента { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }",
+        "assign-new-entity-view": "Назначит новое представление объекта",
+        "delete-entity-view-title": "Вы точно хотите удалить представление объекта '{{entityViewName}}'?",
+        "delete-entity-view-text": "Внимание, после подтверждения представление объекта и все связанные с ним данные будут безвозвратно удалены.",
+        "delete-entity-views-title": "Вы точно хотите удалить { count, plural, 1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }?",
+        "delete-entity-views-action-title": "Удалить { count, plural, 1 {1 представление объекта} few {# представления объектов} 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 представление объекта} few {# представления объектов} 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": "Копировать ИД представление объекта",
+        "assignedToCustomer": "Назначенные клиенту",
+        "unable-entity-view-device-alias-title": "Не удалось удалить псевдоним представления объекта",
+        "unable-entity-view-device-alias-text": "Не удалось удалить псевдоним устройства '{{entityViewAlias}}', т.к. он используется следующими виджетами:<br/>{{widgetsList}}",
+        "select-entity-view": "Выбрать представление объекта",
+        "make-public": "Открыть общий доступ к представлению объекта",
+        "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": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения."
     },
     "event": {
         "event-type": "Тип события",
         "type-error": "Ошибка",
         "type-lc-event": "Событие жизненного цикла",
         "type-stats": "Статистика",
-        "type-debug-rule-node": "Debug",
-        "type-debug-rule-chain": "Debug",
+        "type-debug-rule-node": "Отладка",
+        "type-debug-rule-chain": "Отладка",
         "no-events-prompt": "События не найдены",
         "error": "Ошибка",
         "alarm": "Аварийное оповещение",
@@ -719,14 +877,14 @@
         "server": "Сервер",
         "body": "Тело",
         "method": "Метод",
-        "type": "Type",
-        "entity": "Entity",
-        "message-id": "Message Id",
-        "message-type": "Message Type",
-        "data-type": "Data Type",
-        "relation-type": "Relation Type",
-        "metadata": "Metadata",
-        "data": "Data",
+        "type": "Тип",
+        "entity": "Объект",
+        "message-id": "ИД сообщения",
+        "message-type": "Тип сообщения",
+        "data-type": "Тип данных",
+        "relation-type": "Тип отношения",
+        "metadata": "Метаданные",
+        "data": "Данные",
         "event": "Событие",
         "status": "Статус",
         "success": "Успех",
@@ -735,161 +893,159 @@
         "errors-occurred": "Возникли ошибки"
     },
     "extension": {
-        "extensions": "Extensions",
-        "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected",
-        "type": "Type",
-        "key": "Key",
-        "value": "Value",
-        "id": "Id",
-        "extension-id": "Extension id",
-        "extension-type": "Extension type",
+        "extensions": "Расширение",
+        "selected-extensions": "Выбрано { count, plural, 1 {1 расширение} few {# расширения} other {# расширений} }",
+        "type": "Тип",
+        "key": "Ключ",
+        "value": "Значение",
+        "id": "ИД",
+        "extension-id": "ИД расширения",
+        "extension-type": "Тип расширения",
         "transformer-json": "JSON *",
-        "unique-id-required": "Current extension id already exists.",
-        "delete": "Delete extension",
-        "add": "Add extension",
-        "edit": "Edit extension",
-        "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
-        "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
-        "delete-extensions-title": "Are you sure you want to delete { count, plural, 1 {1 extension} other {# extensions} }?",
-        "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
-        "converters": "Converters",
-        "converter-id": "Converter id",
-        "configuration": "Configuration",
-        "converter-configurations": "Converter configurations",
-        "token": "Security token",
-        "add-converter": "Add converter",
-        "add-config": "Add converter configuration",
-        "device-name-expression": "Device name expression",
-        "device-type-expression": "Device type expression",
-        "custom": "Custom",
+        "unique-id-required": "Такое ИД расширения уже существует.",
+        "delete": "Удалить расширение",
+        "add": "Добавить расширение",
+        "edit": "Редактировать расширение",
+        "delete-extension-title": "Вы точно хотите удалить расширение '{{extensionId}}'?",
+        "delete-extension-text": "Внимание, после подтверждения расширение и все связанные с ним данные будут безвозвратно удалены.",
+        "delete-extensions-title": "Вы точно хотите удалить { count, plural, 1 {1 расширение} few {# расширения} other {# расширений} }?",
+        "delete-extensions-text": "Внимание, после подтверждения выбранные расширения будут удалены.",
+        "converters": "Конвертеры",
+        "converter-id": "ИД конвертера",
+        "configuration": "Конфигурация",
+        "converter-configurations": "Конфигурация конвертера",
+        "token": "Токен безопасности",
+        "add-converter": "Добавить конвертер",
+        "add-config": "Добавить конфигурацию конвертера",
+        "device-name-expression": "Маска названия устройства",
+        "device-type-expression": "Маска типа устройства",
+        "custom": "Пользовательский",
         "to-double": "To Double",
-        "transformer": "Transformer",
-        "json-required": "Transformer json is required.",
-        "json-parse": "Unable to parse transformer json.",
-        "attributes": "Attributes",
-        "add-attribute": "Add attribute",
-        "add-map": "Add mapping element",
-        "timeseries": "Timeseries",
-        "add-timeseries": "Add timeseries",
-        "field-required": "Field is required",
-        "brokers": "Brokers",
-        "add-broker": "Add broker",
-        "host": "Host",
-        "port": "Port",
-        "port-range": "Port should be in a range from 1 to 65535.",
-        "ssl": "Ssl",
-        "credentials": "Credentials",
-        "username": "Username",
-        "password": "Password",
-        "retry-interval": "Retry interval in milliseconds",
-        "anonymous": "Anonymous",
-        "basic": "Basic",
+        "transformer": "Преобразователь",
+        "json-required": "JSON преобразователя обязателен.",
+        "json-parse": "Не удалось распознать JSON преобразователя.",
+        "attributes": "Атрибуты",
+        "add-attribute": "Добавить атрибут",
+        "add-map": "Добавить элемент сопоставления",
+        "timeseries": "Телеметрия",
+        "add-timeseries": "Добавить параметр телеметрии",
+        "field-required": "Параметр обязателен",
+        "brokers": "Брокеры",
+        "add-broker": "Добавить брокер",
+        "host": "Хост",
+        "port": "Порт",
+        "port-range": "Значение порта лежит в диапазоне от 1 до 65535.",
+        "ssl": "SSL",
+        "credentials": "Учетные данные",
+        "username": "Имя пользователя",
+        "password": "Пароль",
+        "retry-interval": "Интервал повтора в миллисекундах",
+        "anonymous": "Анонимный",
+        "basic": "Общий",
         "pem": "PEM",
-        "ca-cert": "CA certificate file *",
-        "private-key": "Private key file *",
-        "cert": "Certificate file *",
-        "no-file": "No file selected.",
-        "drop-file": "Drop a file or click to select a file to upload.",
-        "mapping": "Mapping",
-        "topic-filter": "Topic filter",
-        "converter-type": "Converter type",
-        "converter-json": "Json",
-        "json-name-expression": "Device name json expression",
-        "topic-name-expression": "Device name topic expression",
-        "json-type-expression": "Device type json expression",
-        "topic-type-expression": "Device type topic expression",
-        "attribute-key-expression": "Attribute key expression",
-        "attr-json-key-expression": "Attribute key json expression",
-        "attr-topic-key-expression": "Attribute key topic expression",
-        "request-id-expression": "Request id expression",
-        "request-id-json-expression": "Request id json expression",
-        "request-id-topic-expression": "Request id topic expression",
-        "response-topic-expression": "Response topic expression",
-        "value-expression": "Value expression",
-        "topic": "Topic",
-        "timeout": "Timeout in milliseconds",
-        "converter-json-required": "Converter json is required.",
-        "converter-json-parse": "Unable to parse converter json.",
-        "filter-expression": "Filter expression",
-        "connect-requests": "Connect requests",
-        "add-connect-request": "Add connect request",
-        "disconnect-requests": "Disconnect requests",
-        "add-disconnect-request": "Add disconnect request",
-        "attribute-requests": "Attribute requests",
-        "add-attribute-request": "Add attribute request",
-        "attribute-updates": "Attribute updates",
-        "add-attribute-update": "Add attribute update",
-        "server-side-rpc": "Server side RPC",
-        "add-server-side-rpc-request": "Add server-side RPC request",
-        "device-name-filter": "Device name filter",
-        "attribute-filter": "Attribute filter",
-        "method-filter": "Method filter",
-        "request-topic-expression": "Request topic expression",
-        "response-timeout": "Response timeout in milliseconds",
-        "topic-expression": "Topic expression",
-        "client-scope": "Client scope",
-        "add-device": "Add device",
-        "opc-server": "Servers",
-        "opc-add-server": "Add server",
-        "opc-add-server-prompt": "Please add server",
-        "opc-application-name": "Application name",
-        "opc-application-uri": "Application uri",
-        "opc-scan-period-in-seconds": "Scan period in seconds",
-        "opc-security": "Security",
-        "opc-identity": "Identity",
-        "opc-keystore": "Keystore",
-        "opc-type": "Type",
-        "opc-keystore-type": "Type",
-        "opc-keystore-location": "Location *",
-        "opc-keystore-password": "Password",
-        "opc-keystore-alias": "Alias",
-        "opc-keystore-key-password": "Key password",
-        "opc-device-node-pattern": "Device node pattern",
-        "opc-device-name-pattern": "Device name pattern",
-        "modbus-server": "Servers/slaves",
-        "modbus-add-server": "Add server/slave",
-        "modbus-add-server-prompt": "Please add server/slave",
-        "modbus-transport": "Transport",
-        "modbus-port-name": "Serial port name",
-        "modbus-encoding": "Encoding",
-        "modbus-parity": "Parity",
-        "modbus-baudrate": "Baud rate",
-        "modbus-databits": "Data bits",
-        "modbus-stopbits": "Stop bits",
-        "modbus-databits-range": "Data bits should be in a range from 7 to 8.",
-        "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.",
-        "modbus-unit-id": "Unit ID",
-        "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.",
-        "modbus-device-name": "Device name",
-        "modbus-poll-period": "Poll period (ms)",
-        "modbus-attributes-poll-period": "Attributes poll period (ms)",
-        "modbus-timeseries-poll-period": "Timeseries poll period (ms)",
-        "modbus-poll-period-range": "Poll period should be positive value.",
-        "modbus-tag": "Tag",
-        "modbus-function": "Function",
-        "modbus-register-address": "Register address",
-        "modbus-register-address-range": "Register address should be in a range from 0 to 65535.",
-        "modbus-register-bit-index": "Bit index",
-        "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.",
-        "modbus-register-count": "Register count",
-        "modbus-register-count-range": "Register count should be a positive value.",
-        "modbus-byte-order": "Byte order",
-
+        "ca-cert": "Файл CA сертификата *",
+        "private-key": "Файл приватного ключа *",
+        "cert": "Файл сертификата *",
+        "no-file": "Не выбран файл.",
+        "drop-file": "Перетяните файл или нажмите для выбора файла.",
+        "mapping": "Сопоставление",
+        "topic-filter": "Фильтр тем",
+        "converter-type": "Тип конвертера",
+        "converter-json": "JSON",
+        "json-name-expression": "JSON выражение для названия устройства",
+        "topic-name-expression": "Выражение для названия устройства в названии темы",
+        "json-type-expression": "JSON выражение для типа устройства",
+        "topic-type-expression": "Выражение для типа устройства в названии темы",
+        "attribute-key-expression": "Выражение для атрибута",
+        "attr-json-key-expression": "JSON выражение для атрибута",
+        "attr-topic-key-expression": "Выражение для атрибута в названии темы",
+        "request-id-expression": "Выражение для ИД запроса",
+        "request-id-json-expression": "JSON выражение для ИД запроса",
+        "request-id-topic-expression": "Выражение для ИД запроса в названии темы",
+        "response-topic-expression": "Выражение для темы ответов",
+        "value-expression": "Выражение для значения",
+        "topic": "Тема",
+        "timeout": "Таймаут в миллисекундах",
+        "converter-json-required": "JSON конвертер обязателен.",
+        "converter-json-parse": "Не удалось распознать JSON конвертера.",
+        "filter-expression": "Выражение для фильтрации",
+        "connect-requests": "Запросы о подключении устройства",
+        "add-connect-request": "Добавить запросы о подключении устройства",
+        "disconnect-requests": "Запросы об отсоединении устройства",
+        "add-disconnect-request": "Добавить запрос об отсоединении устройства",
+        "attribute-requests": "Запросы для атрибутов",
+        "add-attribute-request": "Добавить запрос для атрибутов",
+        "attribute-updates": "Обновление атрибутов",
+        "add-attribute-update": "Добавить обновление атрибутов",
+        "server-side-rpc": "Серверный RPC",
+        "add-server-side-rpc-request": "Добавить серверный RPC",
+        "device-name-filter": "Фильтр для названия устройства",
+        "attribute-filter": "Фильтр для атрибутов",
+        "method-filter": "Фильтр для процедур",
+        "request-topic-expression": "Выражение для темы запросов",
+        "response-timeout": "Время ожидания ответа в миллисекундах",
+        "topic-expression": "Выражение для названия темы",
+        "client-scope": "Клиентский",
+        "add-device": "Добавить устройство",
+        "opc-server": "Серверы",
+        "opc-add-server": "Добавить сервер",
+        "opc-add-server-prompt": "Пожалуйста, добавьте сервер",
+        "opc-application-name": "Название приложения",
+        "opc-application-uri": "URI приложения",
+        "opc-scan-period-in-seconds": "Частота сканирования в секундах",
+        "opc-security": "Безопасность",
+        "opc-identity": "Идентификация",
+        "opc-keystore": "Хранилище ключей",
+        "opc-type": "Тип",
+        "opc-keystore-type": "Тип",
+        "opc-keystore-location": "Расположение *",
+        "opc-keystore-password": "Пароль",
+        "opc-keystore-alias": "Псевдоним",
+        "opc-keystore-key-password": "Пароль для ключ",
+        "opc-device-node-pattern": "Паттерн OPC узла устройства",
+        "opc-device-name-pattern": "Паттерн названия устройства",
+        "modbus-server": "Серверы/ведомые устройства",
+        "modbus-add-server": "Добавить сервер/ведомое устройство",
+        "modbus-add-server-prompt": "Пожалуйста, добавить сервер/ведомое устройство",
+        "modbus-transport": "Транспорт",
+        "modbus-port-name": "Название последовательного порта",
+        "modbus-encoding": "Кодирование символов",
+        "modbus-parity": "Паритет",
+        "modbus-baudrate": "Скорость передачи",
+        "modbus-databits": "Биты данных",
+        "modbus-stopbits": "Стоп-биты",
+        "modbus-databits-range": "Параметр \"Биты данных\" может принимать значения 7 или 8.",
+        "modbus-stopbits-range": "Параметр \"Стоп-биты\" может принимать значения 1 или 2.",
+        "modbus-unit-id": "ИД устройства",
+        "modbus-unit-id-range": "ИД устройства должен быть в диапазоне от 1 до 247.",
+        "modbus-device-name": "Название устройства",
+        "modbus-poll-period": "Частота опроса (в миллисекундах)",
+        "modbus-attributes-poll-period": "Частота опроса для атрибутов (в миллисекундах)",
+        "modbus-timeseries-poll-period": "Частота опроса для телеметрии (в миллисекундах)",
+        "modbus-poll-period-range": "Значение параметра \"Частота опроса\" должно быть больше ноля.",
+        "modbus-tag": "Тег",
+        "modbus-function": "Modbus функция",
+        "modbus-register-address": "Адрес регистра",
+        "modbus-register-address-range": "Адрес регистра должен быть в диапазоне от 0 до 65535.",
+        "modbus-register-bit-index": "Номер бита",
+        "modbus-register-bit-index-range": "Номер бита должен быть в диапазоне от 0 до 15.",
+        "modbus-register-count": "Количество регистров",
+        "modbus-register-count-range": "Количество регистров должно быть больше ноля.",
+        "modbus-byte-order": "Порядок байтов",
         "sync": {
-            "status": "Status",
-            "sync": "Sync",
-            "not-sync": "Not sync",
-            "last-sync-time": "Last sync time",
-            "not-available": "Not available"
+            "status": "Статус",
+            "sync": "Синхронизирован",
+            "not-sync": "Не синхронизирован",
+            "last-sync-time": "Время последней синхронизации",
+            "not-available": "Не доступен"
         },
-
-        "export-extensions-configuration": "Export extensions configuration",
-        "import-extensions-configuration": "Import extensions configuration",
-        "import-extensions": "Import extensions",
-        "import-extension": "Import extension",
-        "export-extension": "Export extension",
-        "file": "Extensions file",
-        "invalid-file-error": "Invalid extension file"
+        "export-extensions-configuration": "Экспортировать конфигурацию расширений",
+        "import-extensions-configuration": "Импортировать конфигурацию расширений",
+        "import-extensions": "Импортировать расширения",
+        "import-extension": "Импортировать расширение",
+        "export-extension": "Экспортировать расширение",
+        "file": "Файл расширений",
+        "invalid-file-error": "Не правильный формат файла"
     },
     "fullscreen": {
         "expand": "Во весь экран",
@@ -937,20 +1093,20 @@
         "tidy": "Tidy"
     },
     "key-val": {
-        "key": "Key",
-        "value": "Value",
-        "remove-entry": "Remove entry",
-        "add-entry": "Add entry",
-        "no-data": "No entries"
+        "key": "Ключ",
+        "value": "Значение",
+        "remove-entry": "Удалить элемент",
+        "add-entry": "Добавить элемент",
+        "no-data": "Элементы отсутствуют"
     },
     "layout": {
-        "layout": "Layout",
-        "manage": "Manage layouts",
-        "settings": "Layout settings",
-        "color": "Color",
-        "main": "Main",
-        "right": "Right",
-        "select": "Select target layout"
+        "layout": "Макет",
+        "manage": "Управление макетами",
+        "settings": "Настройки макета",
+        "color": "Цвет",
+        "main": "Основной",
+        "right": "Правый",
+        "select": "Выбрать макет"
     },
     "legend": {
         "position": "Расположение легенды",
@@ -993,140 +1149,149 @@
         "current-password": "Текущий пароль"
     },
     "relation": {
-        "relations": "Relations",
-        "direction": "Direction",
+        "relations": "Отношения",
+        "direction": "Направления",
         "search-direction": {
-            "FROM": "From",
-            "TO": "To"
+            "FROM": "От",
+            "TO": "К"
         },
         "direction-type": {
-            "FROM": "from",
-            "TO": "to"
+            "FROM": "от",
+            "TO": "к"
         },
-        "from-relations": "Outbound relations",
-        "to-relations": "Inbound relations",
-        "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selected",
-        "type": "Type",
-        "to-entity-type": "To entity type",
-        "to-entity-name": "To entity name",
-        "from-entity-type": "From entity type",
-        "from-entity-name": "From entity name",
-        "to-entity": "To entity",
-        "from-entity": "From entity",
-        "delete": "Delete relation",
-        "relation-type": "Relation type",
-        "relation-type-required": "Relation type is required.",
-        "any-relation-type": "Any type",
-        "add": "Add relation",
-        "edit": "Edit relation",
-        "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?",
-        "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.",
-        "delete-to-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?",
-        "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.",
-        "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?",
-        "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.",
-        "delete-from-relations-title": "Are you sure you want to delete { count, plural, 1 {1 relation} other {# relations} }?",
-        "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.",
-        "remove-relation-filter": "Remove relation filter",
-        "add-relation-filter": "Add relation filter",
-        "any-relation": "Any relation",
-        "relation-filters": "Relation filters",
-        "additional-info": "Additional info (JSON)",
-        "invalid-additional-info": "Unable to parse additional info json."
+        "from-relations": "Исходящие отношения",
+        "to-relations": "Входящие отношения",
+        "selected-relations": "Выбрано { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }",
+        "type": "Тип",
+        "to-entity-type": "К типу объекта",
+        "to-entity-name": "К объекта",
+        "from-entity-type": "От типа объекта",
+        "from-entity-name": "От объекта",
+        "to-entity": "К объекту",
+        "from-entity": "От объекта",
+        "delete": "Удалить отношение",
+        "relation-type": "Тип отношения",
+        "relation-type-required": "Тип отношения обязателен.",
+        "any-relation-type": "Любой тип",
+        "add": "Добавить отношение",
+        "edit": "Редактировать отношение",
+        "delete-to-relation-title": "Вы точно хотите удалить отношение, идущее к объекту '{{entityName}}'?",
+        "delete-to-relation-text": "Внимание, после подтверждения объект '{{entityName}}' будет отвязан от текущего объекта.",
+        "delete-to-relations-title": "Вы точно хотите удалить { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }?",
+        "delete-to-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от текущего объекта.",
+        "delete-from-relation-title": "Вы точно хотите удалить отношение, идущее от объекта '{{entityName}}'?",
+        "delete-from-relation-text": "Внимание, после подтверждения текущий объект будет отвязан от объекта '{{entityName}}'.",
+        "delete-from-relations-title": "Вы точно хотите удалить { count, plural, 1 {1 отношение} few {# отношения} other {# отношений} }?",
+        "delete-from-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от соответствующих объектов.",
+        "remove-relation-filter": "Удалить фильтр отношений",
+        "add-relation-filter": "Добавить фильтр отношений",
+        "any-relation": "Любое отношение",
+        "relation-filters": "Фильтры отношений",
+        "additional-info": "Дополнительная информация (JSON)",
+        "invalid-additional-info": "Не удалось распознать JSON с дополнительной информацией."
     },
     "rulechain": {
-        "rulechain": "Rule chain",
-        "rulechains": "Rule chains",
-        "root": "Root",
-        "delete": "Delete rule chain",
-        "name": "Name",
-        "name-required": "Name is required.",
-        "description": "Description",
-        "add": "Add Rule Chain",
-        "set-root": "Make rule chain root",
-        "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?",
-        "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.",
-        "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
-        "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
-        "delete-rulechains-title": "Are you sure you want to delete { count, plural, 1 {1 rule chain} other {# rule chains} }?",
-        "delete-rulechains-action-title": "Delete { count, plural, 1 {1 rule chain} other {# rule chains} }",
-        "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
-        "add-rulechain-text": "Add new rule chain",
-        "no-rulechains-text": "No rule chains found",
-        "rulechain-details": "Rule chain details",
-        "details": "Details",
-        "events": "Events",
-        "system": "System",
-        "import": "Import rule chain",
-        "export": "Export rule chain",
-        "export-failed-error": "Unable to export rule chain: {{error}}",
-        "create-new-rulechain": "Create new rule chain",
-        "rulechain-file": "Rule chain file",
-        "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
-        "copyId": "Copy rule chain Id",
-        "idCopiedMessage": "Rule chain Id has been copied to clipboard",
-        "select-rulechain": "Select rule chain",
-        "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
-        "rulechain-required": "Rule chain is required",
-        "management": "Rules management",
-        "debug-mode": "Debug mode"
+        "rulechain": "Цепочка правил",
+        "rulechains": "Цепочки правил",
+        "root": "Корневая",
+        "delete": "Удалить цепочку правил",
+        "name": "Названия",
+        "name-required": "Название необходимо.",
+        "description": "Описание",
+        "add": "Добавить цепочку правил",
+        "set-root": "Сделать цепочку корневой",
+        "set-root-rulechain-title": "Вы точно хотите сделать цепочку правил '{{ruleChainName}}' корневой?",
+        "set-root-rulechain-text": "После подтверждения цепочка правил станет корневой и будет обрабатывать все входящие сообщения.",
+        "delete-rulechain-title": "Вы точно хотите удалить цепочку правил '{{ruleChainName}}'?",
+        "delete-rulechain-text": "Внимание, после подтверждения цепочка правил и все связанные с ней данные будут безвозвратно удалены.",
+        "delete-rulechains-title": "Вы точно хотите удалить { count, plural, 1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }?",
+        "delete-rulechains-action-title": "Удалить { count, plural, 1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }",
+        "delete-rulechains-text": "Внимание, после подтверждения выбранные цепочки правил и все связанные с ними данные будут безвозвратно удалены.",
+        "add-rulechain-text": "Добавить новую цепочку правил",
+        "no-rulechains-text": "Цепочки правил не найдены",
+        "rulechain-details": "Подробности о цепочке правил",
+        "details": "Подробности",
+        "events": "События",
+        "system": "Системная",
+        "import": "Импортировать цепочку правил",
+        "export": "Экспортировать цепочку правил",
+        "export-failed-error": "Не удалось экспортировать цепочку правил: {{error}}",
+        "create-new-rulechain": "Создать новую цепочку правил",
+        "rulechain-file": "Файл цепочки правил",
+        "invalid-rulechain-file-error": "Не удалось импортировать цепочку правил: неправильный формат.",
+        "copyId": "Копировать ИД цепочки правил",
+        "idCopiedMessage": "ИД цепочки правил скопирован в буфер обмена",
+        "select-rulechain": "Выбрать цепочку правил",
+        "no-rulechains-matching": "Цепочки правил, соответствующие '{{entity}}', не найдены.",
+        "rulechain-required": "Цепочка правил обязательна",
+        "management": "Управление цепочками правил",
+        "debug-mode": "Режим отладки"
     },
     "rulenode": {
-        "details": "Details",
-        "events": "Events",
-        "search": "Search nodes",
-        "open-node-library": "Open node library",
-        "add": "Add rule node",
-        "name": "Name",
-        "name-required": "Name is required.",
-        "type": "Type",
-        "description": "Description",
-        "delete": "Delete rule node",
-        "select-all-objects": "Select all nodes and connections",
-        "deselect-all-objects": "Deselect all nodes and connections",
-        "delete-selected-objects": "Delete selected nodes and connections",
-        "delete-selected": "Delete selected",
-        "select-all": "Select all",
-        "copy-selected": "Copy selected",
-        "deselect-all": "Deselect all",
-        "rulenode-details": "Rule node details",
-        "debug-mode": "Debug mode",
-        "configuration": "Configuration",
-        "link": "Link",
-        "link-details": "Rule node link details",
-        "add-link": "Add link",
-        "link-label": "Link label",
-        "link-label-required": "Link label is required.",
-        "custom-link-label": "Custom link label",
-        "custom-link-label-required": "Custom link label is required.",
-        "type-filter": "Filter",
-        "type-filter-details": "Filter incoming messages with configured conditions",
-        "type-enrichment": "Enrichment",
-        "type-enrichment-details": "Add additional information into Message Metadata",
-        "type-transformation": "Transformation",
-        "type-transformation-details": "Change Message payload and Metadata",
-        "type-action": "Action",
-        "type-action-details": "Perform special action",
-        "type-external": "External",
-        "type-external-details": "Interacts with external system",
-        "type-rule-chain": "Rule Chain",
-        "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
-        "type-input": "Input",
-        "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node",
-        "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
-        "ui-resources-load-error": "Failed to load configuration ui resources.",
-        "invalid-target-rulechain": "Unable to resolve target rule chain!",
-        "test-script-function": "Test script function",
-        "message": "Message",
-        "message-type": "Message type",
-        "message-type-required": "Message type is required",
-        "metadata": "Metadata",
-        "metadata-required": "Metadata entries can't be empty.",
-        "output": "Output",
-        "test": "Test",
-        "help": "Help"
+        "details": "Подробности",
+        "events": "События",
+        "search": "Поиск правил",
+        "open-node-library": "Открыть библиотеку правил",
+        "add": "Добавить правило",
+        "name": "Название",
+        "name-required": "Название обязательно.",
+        "type": "Тип",
+        "description": "Описание",
+        "delete": "Удалить правило",
+        "select-all-objects": "Выделить все правила и связи",
+        "deselect-all-objects": "Отменить выделение правил и связей",
+        "delete-selected-objects": "Удалить выделенные правила и связи",
+        "delete-selected": "Удалить выделенные",
+        "select-all": "Выделить всё",
+        "copy-selected": "Копировать выделенное",
+        "deselect-all": "Отменить выделение",
+        "rulenode-details": "Подробности о правиле",
+        "debug-mode": "Режим отладки",
+        "configuration": "Настройки",
+        "link": "Связь",
+        "link-details": "Подробности о связи правила",
+        "add-link": "Добавить связь",
+        "link-label": "Метка связи",
+        "link-label-required": "Метка связи обязателен.",
+        "custom-link-label": "Пользовательская метка связи",
+        "custom-link-label-required": "Пользовательская метка связи обязателен.",
+        "link-labels": "Метки связи",
+        "link-labels-required": "Метки связи обязательны.",
+        "no-link-labels-found": "Метки связи не найдены",
+        "no-link-label-matching": "Метка '{{label}}' не найдена.",
+        "create-new-link-label": "Создать новую!",
+        "type-filter": "Фильтр",
+        "type-filter-details": "Фильтр входящих сообщений с заданными условиями",
+        "type-enrichment": "Насыщение",
+        "type-enrichment-details": "Добавить данные в метадату сообщения",
+        "type-transformation": "Преобразование",
+        "type-transformation-details": "Изменить содержимое сообщение и его метадату",
+        "type-action": "Действие",
+        "type-action-details": "Выполнить заданное действие",
+        "type-external": "Сторонние",
+        "type-external-details": "Взаимодействовать со сторонними системами",
+        "type-rule-chain": "Цепочка правил",
+        "type-rule-chain-details": "Перенаправить входящее сообщение в другую цепочку правил",
+        "type-input": "Вход",
+        "type-input-details": "Логический вход цепочки правил перенаправляет входящие сообщения в следующее правило",
+        "type-unknown": "Неизвестный",
+        "type-unknown-details": "Неопределенное правило",
+        "directive-is-not-loaded": "Указанная директива конфигурации '{{directiveName}}' не доступна.",
+        "ui-resources-load-error": "Не удалось загрузить UI ресурсы.",
+        "invalid-target-rulechain": "Не удалось определить целевую цепочку правил!",
+        "test-script-function": "Протестировать скрипт",
+        "message": "Сообщение",
+        "message-type": "Тип сообщения",
+        "select-message-type": "Выбрать тип сообщения",
+        "message-type-required": "Тип сообщения обязателен",
+        "metadata": "Метаданные",
+        "metadata-required": "Метаданные объекта не могут быть пустыми.",
+        "output": "Выход",
+        "test": "Протестировать",
+        "help": "Помощь"
     },
     "tenant": {
+        "tenant": "Владелец",
         "tenants": "Владельцы",
         "management": "Управление владельцами",
         "add": "Добавить владельца",
@@ -1143,7 +1308,14 @@
         "delete-tenants-text": "Внимание, после подтверждения выбранные Владельцы и все связанные с ними данные будут безвозвратно утеряны.",
         "title": "Имя",
         "title-required": "Имя обязательно.",
-        "description": "Описание"
+        "description": "Описание",
+        "details": "Подробности",
+        "events": "События",
+        "copyId": "Копировать ИД владельца",
+        "idCopiedMessage": "ИД владельца скопирован в буфер обмена",
+        "select-tenant": "Выбрать владельца",
+        "no-tenants-matching": "Владельцы, соответствующие '{{entity}}', не найдены.",
+        "tenant-required": "Владелец обязателен"
     },
     "timeinterval": {
         "seconds-interval": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }",
@@ -1171,6 +1343,7 @@
         "time-period": "Период времени"
     },
     "user": {
+        "user": "Пользователь",
         "users": "Пользователи",
         "customer-users": "Пользователи клиента",
         "tenant-admins": "Администраторы владельца",
@@ -1192,11 +1365,25 @@
         "resend-activation": "Повторить отправку активационного письма",
         "email": "Эл. адрес",
         "email-required": "Эл. адрес обязателен.",
+        "invalid-email-format": "Не правильный формат письма.",
         "first-name": "Имя",
         "last-name": "Фамилия",
         "description": "Описание",
         "default-dashboard": "Дашборд по умолчанию",
-        "always-fullscreen": "Всегда во весь экран"
+        "always-fullscreen": "Всегда в полноэкранном режиме",
+        "select-user": "Выбрать пользователя",
+        "no-users-matching": "Пользователи, соответствующие '{{entity}}', не найдены.",
+        "user-required": "Пользователь обязателен",
+        "activation-method": "Метод активации",
+        "display-activation-link": "Отобразить ссылку для активации",
+        "send-activation-mail": "Отправить активационное письмо",
+        "activation-link": "Активационная ссылка для пользователя",
+        "activation-link-text": "Для активации пользователя используйте <a href='{{activationLink}}' target='_blank'>ссылку</a> :",
+        "copy-activation-link": "Копировать активационную ссылку",
+        "activation-link-copied-message": "Ссылка для активации пользователя скопировано в буфер обмена",
+        "details": "Подробности",
+        "login-as-tenant-admin": "Войти как администратор владельца",
+        "login-as-customer-user": "Войти как пользователь клиента"
     },
     "value": {
         "type": "Тип значения",
@@ -1210,7 +1397,8 @@
         "boolean": "Логический тип",
         "boolean-value": "Логическое значение",
         "false": "Ложь",
-        "true": "Правда"
+        "true": "Правда",
+        "long": "Целое число"
     },
     "widget": {
         "widget-library": "Галерея виджетов",
@@ -1225,8 +1413,11 @@
         "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?",
         "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.",
         "timeseries": "Выборка по времени",
+        "search-data": "Search data",
+        "no-data-found": "No data found",
         "latest-values": "Последние значения",
         "rpc": "Управляющий виджет",
+        "alarm": "Alarm widget",
         "static": "Статический виджет",
         "select-widget-type": "Выберите тип виджета",
         "missing-widget-title-error": "Укажите название виджета!",
@@ -1262,16 +1453,16 @@
         "export": "Экспортировать виджет"
     },
     "widget-action": {
-        "header-button": "Widget header button",
-        "open-dashboard-state": "Navigate to new dashboard state",
-        "update-dashboard-state": "Update current dashboard state",
-        "open-dashboard": "Navigate to other dashboard",
-        "custom": "Custom action",
-        "target-dashboard-state": "Target dashboard state",
-        "target-dashboard-state-required": "Target dashboard state is required",
-        "set-entity-from-widget": "Set entity from widget",
-        "target-dashboard": "Target dashboard",
-        "open-right-layout": "Open right dashboard layout (mobile view)"
+        "header-button": "Кнопка заголовка виджета",
+        "open-dashboard-state": "Перейти к новомум состоянию дашборда",
+        "update-dashboard-state": "Обновить текущее состояние дашборда",
+        "open-dashboard": "Перейти к другому дашборду",
+        "custom": "Пользовательское действие",
+        "target-dashboard-state": "Целевое состояние дашборда",
+        "target-dashboard-state-required": "Целевое состояние дашборда обязательно",
+        "set-entity-from-widget": "Установить объект из виджета",
+        "target-dashboard": "Целевой дашборд",
+        "open-right-layout": "Открыть мобильный режим дашборда"
     },
     "widgets-bundle": {
         "current": "Текущий набор",
@@ -1312,6 +1503,8 @@
         "background-color": "Цвет фона",
         "text-color": "Цвет текста",
         "padding": "Отступ",
+        "margin": "Margin",
+        "widget-style": "Стиль виджета",
         "title-style": "Стиль названия",
         "mobile-mode-settings": "Настройки мобильного режима",
         "order": "Порядок",
@@ -1322,11 +1515,29 @@
         "use-dashboard-timewindow": "Использовать временное окно дашборда",
         "display-legend": "Показать легенду",
         "datasources": "Источники данных",
+        "maximum-datasources": "Максимальной количество источников данных равно {{count}}",
         "datasource-type": "Тип",
         "datasource-parameters": "Параметры",
         "remove-datasource": "Удалить источник данных",
         "add-datasource": "Добавить источник данных",
-        "target-device": "Целевое устройство"
+        "target-device": "Целевое устройство",
+        "alarm-source": "Источник оповещения",
+        "actions": "Действия",
+        "action": "Действие",
+        "add-action": "Добавить действие",
+        "search-actions": "Поиск действий",
+        "action-source": "Источник действий",
+        "action-source-required": "Источник действий обязателен.",
+        "action-name": "Название",
+        "action-name-required": "Название действия обязательно.",
+        "action-name-not-unique": "Действие с таким именем уже существует.<br/>Название должно быть уникально в рамках одного источника действий.",
+        "action-icon": "Иконка",
+        "action-type": "Тип",
+        "action-type-required": "Тип действий обязателен.",
+        "edit-action": "Редактировать действие",
+        "delete-action": "Удалить действие",
+        "delete-action-title": "Удалить действие виджета",
+        "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?"
     },
     "widget-type": {
         "import": "Импортировать тип виджета",
@@ -1337,32 +1548,32 @@
         "invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета."
     },
     "icon": {
-        "icon": "Icon",
-        "select-icon": "Select icon",
-        "material-icons": "Material icons",
-        "show-all": "Show all icons"
+        "icon": "Иконка",
+        "select-icon": "Выбрать иконку",
+        "material-icons": "Иконки в стиле Material",
+        "show-all": "Показать все иконки"
     },
     "custom": {
         "widget-action": {
-            "action-cell-button": "Action cell button",
-            "row-click": "On row click",
-            "marker-click": "On marker click",
-            "tooltip-tag-action": "Tooltip tag action"
+            "action-cell-button": "Кнопка действия ячейки",
+            "row-click": "Действий при щелчке на строку",
+            "marker-click": "Действия при щелчке на указателе",
+            "tooltip-tag-action": "Действие при подсказке"
         }
     },
     "language": {
         "language": "Язык",
         "locales": {
+            "de_DE": "Немецкий",
             "en_US": "Английский",
-			"fr_FR": "Французский",
             "zh_CN": "Китайский",
             "ko_KR": "Корейский",
             "es_ES": "Испанский",
             "it_IT": "Итальянский",
             "ru_RU": "Русский",
-            "ja_JA": "Японский",
-            "tr_TR": "Турецкий"
+            "tr_TR": "Турецкий",
+            "fr_FR": "Французский",
+            "ja_JA": "Японский"
         }
-
     }
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-tr_TR.json b/ui/src/app/locale/locale.constant-tr_TR.json
index 54176d8..3f2e37e 100644
--- a/ui/src/app/locale/locale.constant-tr_TR.json
+++ b/ui/src/app/locale/locale.constant-tr_TR.json
@@ -1175,7 +1175,7 @@
         "delete-rulechains-action-title": "{ count, plural, 1 {1 kuralı} other {# kuralı} } sil",
         "delete-rulechains-text": "UYARI: Onaylandıktan sonra seçili tüm kurallar ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.",
         "add-rulechain-text": "Yeni kural ekle",
-        "no-rulechains-text":"Hiçbir kural bulunamadı",
+        "no-rulechains-text": "Hiçbir kural bulunamadı",
         "rulechain-details": "Kural detayları",
         "details": "Detaylar",
         "events": "Olaylar",
@@ -1531,6 +1531,7 @@
     "language": {
         "language": "Dil",
         "locales": {
+            "de_DE": "Almanca",
             "fr_FR": "Fransızca",
             "zh_CN": "Çince",
             "en_US": "İngilizce",
diff --git a/ui/src/app/locale/locale.constant-zh_CN.json b/ui/src/app/locale/locale.constant-zh_CN.json
index bac1706..bacddc4 100644
--- a/ui/src/app/locale/locale.constant-zh_CN.json
+++ b/ui/src/app/locale/locale.constant-zh_CN.json
@@ -916,7 +916,6 @@
             "last-sync-time": "最后同步时间",
             "not-available": "无法使用"
         },
-
         "export-extensions-configuration": "导出扩展配置",
         "import-extensions-configuration": "导入扩展配置",
         "import-extensions": "导入扩展",
@@ -1299,7 +1298,6 @@
         "saveAs": "部件另存为",
         "save-widget-type-as": "部件类型另存为",
         "save-widget-type-as-text": "请输入新的部件标题或选择目标部件包",
-
         "toggle-fullscreen": "切换全屏",
         "run": "运行部件",
         "title": "部件标题",
@@ -1437,8 +1435,9 @@
     "language": {
         "language": "语言",
         "locales": {
+            "de_DE": "德语",
             "en_US": "英语",
-			"fr_FR": "法国",
+            "fr_FR": "法国",
             "ko_KR": "韩语",
             "zh_CN": "汉语",
             "ru_RU": "俄语",
@@ -1448,4 +1447,4 @@
             "tr_TR": "土耳其"
         }
     }
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js
index ccf931a..3f7be4f 100644
--- a/ui/src/app/widget/lib/google-map.js
+++ b/ui/src/app/widget/lib/google-map.js
@@ -314,6 +314,48 @@ export default class TbGoogleMap {
         polyline.setMap(null);
     }
 
+
+	createPolygon(latLangs, settings) {
+		let polygon = new google.maps.Polygon({
+			map: this.map,
+			paths: latLangs,
+			strokeColor: settings.polygonStrokeColor,
+			strokeOpacity: settings.polygonStrokeColor,
+			fillColor: settings.polygonColor,
+			fillOpacity: settings.polygonOpacity,
+			strokeWeight: settings.polygonStrokeWeight
+		});
+		return polygon;
+	}
+	/* eslint-disable no-undef */
+
+	removePolygon (polygon) {
+		polygon.setMap(null);
+	}
+
+	/* eslint-disable no-undef,no-unused-vars */
+	updatePolygonColor (polygon, settings, color) {
+		let options = {
+			paths: polygon.getPaths(),
+			map: this.map,
+			strokeColor: color,
+			fillColor: color,
+			strokeWeight: settings.polygonStrokeWeight
+		}
+
+	}
+	/* eslint-disable no-undef ,no-unused-vars*/
+
+
+	getPolygonLatLngs(polygon) {
+		return polygon.getPaths().getArray();
+	}
+
+	setPolygonLatLngs(polygon, latLngs) {
+		polygon.setPaths(latLngs);
+	}
+
+
     /* eslint-disable no-undef */
     fitBounds(bounds) {
         if (this.dontFitMapBounds && this.defaultZoomLevel) {
diff --git a/ui/src/app/widget/lib/image-map.js b/ui/src/app/widget/lib/image-map.js
index 74f5841..3dbf536 100644
--- a/ui/src/app/widget/lib/image-map.js
+++ b/ui/src/app/widget/lib/image-map.js
@@ -369,6 +369,21 @@ export default class TbImageMap {
     removePolyline(/*polyline*/) {
     }
 
+	createPolygon(/*latLangs, settings*/) {
+	}
+
+	removePolygon(/*latLangs, settings*/) {
+	}
+
+	updatePolygonColor(/*latLangs, settings*/) {
+	}
+
+	getPolygonLatLngs(/*latLangs, settings*/) {
+	}
+
+	setPolygonLatLngs(/*latLangs, settings*/) {
+	}
+
     fitBounds() {
     }
 
diff --git a/ui/src/app/widget/lib/map-widget2.js b/ui/src/app/widget/lib/map-widget2.js
index 0ed70c5..fed9cb5 100644
--- a/ui/src/app/widget/lib/map-widget2.js
+++ b/ui/src/app/widget/lib/map-widget2.js
@@ -23,1064 +23,1207 @@ import TbTencentMap from './tencent-map';
 import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils';
 
 export default class TbMapWidgetV2 {
-    constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
-        var tbMap = this;
-        this.ctx = ctx;
-        this.mapProvider = mapProvider;
-        if (!$element) {
-            $element = ctx.$container;
-        }
-        this.utils = ctx.$scope.$injector.get('utils');
-        this.drawRoutes = drawRoutes;
-        this.markers = [];
-        if (this.drawRoutes) {
-            this.polylines = [];
-        }
-
-        this.locationSettings = {};
-
-        var settings = ctx.settings;
-
-        this.callbacks = {};
-        this.callbacks.onLocationClick = function(){};
-
-        if (settings.defaultZoomLevel) {
-            if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {
-                this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel);
-            }
-        }
-
-        this.dontFitMapBounds = settings.fitMapBounds === false;
-
-        if (!useDynamicLocations) {
-            this.subscription = this.ctx.defaultSubscription;
-        }
-
-        this.configureLocationsSettings();
-
-        var minZoomLevel = this.drawRoutes ? 18 : 15;
-
-        var initCallback = function() {
-            tbMap.update();
-            tbMap.resize();
-        };
-
-        this.ctx.$scope.onTooltipAction = function(event, actionName, dsIndex) {
-            tbMap.onTooltipAction(event, actionName, dsIndex);
-        };
-        this.tooltipActionsMap = {};
-        var descriptors = this.ctx.actionsApi.getActionDescriptors('tooltipAction');
-        descriptors.forEach(function (descriptor) {
-            tbMap.tooltipActionsMap[descriptor.name] = descriptor;
-        });
-
-        if (mapProvider === 'google-map') {
-            this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, 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);
-        } else if (mapProvider === 'image-map') {
-            this.map = new TbImageMap(this.ctx, $element, this.utils, initCallback,
-                settings.mapImageUrl,
-                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);
-        }
-    }
-
-    setCallbacks(callbacks) {
-        Object.assign(this.callbacks, callbacks);
-    }
-
-    clearLocations() {
-        if (this.locations) {
-            var tbMap = this;
-            this.locations.forEach(function(location) {
-                if (location.marker) {
-                    tbMap.map.removeMarker(location.marker);
-                }
-                if (location.polyline) {
-                    tbMap.map.removePolyline(location.polyline);
-                }
-            });
-            this.locations = null;
-            this.markers = [];
-            if (this.drawRoutes) {
-                this.polylines = [];
-            }
-        }
-    }
-
-    setSubscription(subscription) {
-        this.subscription = subscription;
-        this.clearLocations();
-    }
-
-    configureLocationsSettings() {
-
-        if (this.mapProvider  == 'image-map') {
-            this.locationSettings.latKeyName = this.ctx.settings.xPosKeyName || 'xPos';
-            this.locationSettings.lngKeyName = this.ctx.settings.yPosKeyName || 'yPos';
-            this.locationSettings.markerOffsetX = angular.isDefined(this.ctx.settings.markerOffsetX) ? this.ctx.settings.markerOffsetX : 0.5;
-            this.locationSettings.markerOffsetY = angular.isDefined(this.ctx.settings.markerOffsetY) ? this.ctx.settings.markerOffsetY : 1;
-        } else {
-            this.locationSettings.latKeyName = this.ctx.settings.latKeyName || 'latitude';
-            this.locationSettings.lngKeyName = this.ctx.settings.lngKeyName || 'longitude';
-        }
-
-        this.locationSettings.tooltipPattern = this.ctx.settings.tooltipPattern || "<b>${entityName}</b><br/><br/><b>Latitude:</b> ${"+this.locationSettings.latKeyName+":7}<br/><b>Longitude:</b> ${"+this.locationSettings.lngKeyName+":7}";
-
-        this.locationSettings.showLabel = this.ctx.settings.showLabel !== false;
-        this.locationSettings.displayTooltip = this.ctx.settings.showTooltip !== false;
-        this.locationSettings.autocloseTooltip = this.ctx.settings.autocloseTooltip !== false;
-        this.locationSettings.labelColor = this.ctx.widgetConfig.color || '#000000',
-        this.locationSettings.label = this.ctx.settings.label || "${entityName}";
-        this.locationSettings.color = this.ctx.settings.color ? tinycolor(this.ctx.settings.color).toHexString() : "#FE7569";
-
-        this.locationSettings.useLabelFunction = this.ctx.settings.useLabelFunction === true;
-        if (angular.isDefined(this.ctx.settings.labelFunction) && this.ctx.settings.labelFunction.length > 0) {
-            try {
-                this.locationSettings.labelFunction = new Function('data, dsData, dsIndex', this.ctx.settings.labelFunction);
-            } catch (e) {
-                this.locationSettings.labelFunction = null;
-            }
-        }
-
-        this.locationSettings.useTooltipFunction = this.ctx.settings.useTooltipFunction === true;
-        if (angular.isDefined(this.ctx.settings.tooltipFunction) && this.ctx.settings.tooltipFunction.length > 0) {
-            try {
-                this.locationSettings.tooltipFunction = new Function('data, dsData, dsIndex', this.ctx.settings.tooltipFunction);
-            } catch (e) {
-                this.locationSettings.tooltipFunction = null;
-            }
-        }
-
-        this.locationSettings.useColorFunction = this.ctx.settings.useColorFunction === true;
-        if (angular.isDefined(this.ctx.settings.colorFunction) && this.ctx.settings.colorFunction.length > 0) {
-            try {
-                this.locationSettings.colorFunction = new Function('data, dsData, dsIndex', this.ctx.settings.colorFunction);
-            } catch (e) {
-                this.locationSettings.colorFunction = null;
-            }
-        }
-
-        this.locationSettings.useMarkerImageFunction = this.ctx.settings.useMarkerImageFunction === true;
-        if (angular.isDefined(this.ctx.settings.markerImageFunction) && this.ctx.settings.markerImageFunction.length > 0) {
-            try {
-                this.locationSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', this.ctx.settings.markerImageFunction);
-            } catch (e) {
-                this.locationSettings.markerImageFunction = null;
-            }
-        }
-
-        this.locationSettings.markerImages = this.ctx.settings.markerImages || [];
-
-        if (!this.locationSettings.useMarkerImageFunction &&
-            angular.isDefined(this.ctx.settings.markerImage) &&
-            this.ctx.settings.markerImage.length > 0) {
-            this.locationSettings.useMarkerImage = true;
-            var url = this.ctx.settings.markerImage;
-            var size = this.ctx.settings.markerImageSize || 34;
-            this.locationSettings.currentImage = {
-                url: url,
-                size: size
-            };
-        }
-
-        if (this.drawRoutes) {
-            this.locationSettings.strokeWeight = this.ctx.settings.strokeWeight || 2;
-            this.locationSettings.strokeOpacity = this.ctx.settings.strokeOpacity || 1.0;
-        }
-    }
-
-    onTooltipAction(event, actionName, dsIndex) {
-        var descriptor = this.tooltipActionsMap[actionName];
-        if (descriptor) {
-            var datasource = this.subscription.datasources[dsIndex];
-            var entityId = {};
-            entityId.id = datasource.entityId;
-            entityId.entityType = datasource.entityType;
-            var entityName = datasource.entityName;
-            this.ctx.actionsApi.handleWidgetAction(event, descriptor, entityId, entityName);
-        }
-    }
-
-    update() {
-
-        var tbMap = this;
-
-        function updateLocationLabel(location, dataMap) {
-            if (location.settings.showLabel) {
-                if (location.settings.useLabelFunction && location.settings.labelFunction) {
-                    try {
-                        location.settings.label = location.settings.labelFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
-                    } catch (e) {
-                        location.settings.label = null;
-                    }
-                    if (location.settings.label) {
-                        var datasources = tbMap.subscription.datasources;
-                        location.settings.label = tbMap.utils.createLabelFromDatasource(datasources[location.dsIndex], location.settings.label);
-                        location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, location.dsIndex);
-                        location.settings.labelText = location.settings.label;
-                    }
-                }
-                if (location.settings.labelReplaceInfo.variables.length) {
-                    location.settings.labelText = fillPattern(location.settings.label,
-                        location.settings.labelReplaceInfo, tbMap.subscription.data);
-                }
-                tbMap.map.updateMarkerLabel(location.marker, location.settings);
-            }
-        }
-
-
-        function calculateLocationColor(location, dataMap) {
-            if (location.settings.useColorFunction && location.settings.colorFunction) {
-                var color;
-                try {
-                    color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
-                } catch (e) {/**/}
-                if (!color) {
-                    color = '#FE7569';
-                }
-                return tinycolor(color).toHexString();
-            } else {
-                return location.settings.color;
-            }
-        }
-
-        function updateLocationColor(location, color, image) {
-            if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) {
-                if (!location.settings.useMarkerImage && !image) {
-                    tbMap.map.updateMarkerColor(location.marker, color);
-                }
-                if (location.polyline) {
-                    tbMap.map.updatePolylineColor(location.polyline, location.settings, color);
-                }
-                location.settings.calculatedColor = color;
-            }
-        }
-
-        function calculateLocationMarkerImage(location, dataMap) {
-            if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) {
-                var image = null;
-                try {
-                    image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex);
-                } catch (e) {
-                    image = null;
-                }
-                return image;
-            } else {
-                return null;
-            }
-        }
-
-        function updateLocationMarkerIcon(location, image) {
-            if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) {
-                location.settings.currentImage = image;
-                tbMap.map.updateMarkerIcon(location.marker, location.settings);
-            }
-        }
-
-        function updateLocationStyle(location, dataMap) {
-            updateLocationLabel(location, dataMap);
-            var color = calculateLocationColor(location, dataMap);
-            var image = calculateLocationMarkerImage(location, dataMap);
-            updateLocationColor(location, color, image);
-            updateLocationMarkerIcon(location, image);
-        }
-
-        function createOrUpdateLocationMarker(location, markerLocation, dataMap) {
-            var changed = false;
-            if (!location.marker) {
-                var image = calculateLocationMarkerImage(location, dataMap);
-                if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) {
-                    location.settings.currentImage = image;
-                }
-                location.marker = tbMap.map.createMarker(markerLocation, location.dsIndex, location.settings,
-                    function (event) {
-                        tbMap.callbacks.onLocationClick(location);
-                        locationRowClick(event, location);
-                    }, [location.dsIndex]);
-                tbMap.markers.push(location.marker);
-                changed = true;
-            } else {
-                var prevPosition = tbMap.map.getMarkerPosition(location.marker);
-                if (!prevPosition.equals(markerLocation)) {
-                    tbMap.map.setMarkerPosition(location.marker, markerLocation);
-                    changed = true;
-                }
-            }
-            return changed;
-        }
-
-        function locationRowClick($event, location) {
-            var descriptors = tbMap.ctx.actionsApi.getActionDescriptors('markerClick');
-            if (descriptors.length) {
-                var datasource = tbMap.subscription.datasources[location.dsIndex];
-                var entityId = {};
-                entityId.id = datasource.entityId;
-                entityId.entityType = datasource.entityType;
-                var entityName = datasource.entityName;
-                tbMap.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName);
-            }
-        }
-
-        function updateLocation(location, data, dataMap) {
-            var locationChanged = false;
-            if (location.latIndex > -1 && location.lngIndex > -1) {
-                var latData = data[location.latIndex].data;
-                var lngData = data[location.lngIndex].data;
-                var lat, lng, latLng;
-                if (latData.length > 0 && lngData.length > 0) {
-                    if (tbMap.drawRoutes) {
-                        // Create or update route
-                        var latLngs = [];
-                        for (var i = 0; i < latData.length; i++) {
-                            lat = latData[i][1];
-                            lng = lngData[i][1];
-                            if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) {
-                                latLng = tbMap.map.createLatLng(lat, lng);
-                                if (i == 0 || !latLngs[latLngs.length - 1].equals(latLng)) {
-                                    latLngs.push(latLng);
-                                }
-                            }
-                        }
-                        if (latLngs.length > 0) {
-                            var markerLocation = latLngs[latLngs.length - 1];
-                            createOrUpdateLocationMarker(location, markerLocation, dataMap);
-                        }
-                        if (!location.polyline) {
-                            location.polyline = tbMap.map.createPolyline(latLngs, location.settings);
-                            tbMap.polylines.push(location.polyline);
-                            locationChanged = true;
-                        } else {
-                            var prevPath = tbMap.map.getPolylineLatLngs(location.polyline);
-                            if (!prevPath || !arraysEqual(prevPath, latLngs)) {
-                                tbMap.map.setPolylineLatLngs(location.polyline, latLngs);
-                                locationChanged = true;
-                            }
-                        }
-                    } else {
-                        // Create or update marker
-                        lat = latData[latData.length - 1][1];
-                        lng = lngData[lngData.length - 1][1];
-                        if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) {
-                            latLng = tbMap.map.createLatLng(lat, lng);
-                            if (createOrUpdateLocationMarker(location, latLng, dataMap)) {
-                                locationChanged = true;
-                            }
-                        }
-                    }
-                    if (location.marker) {
-                        updateLocationStyle(location, dataMap);
-                    }
-                }
-            }
-            return locationChanged;
-        }
-
-        function loadLocations(data, datasources) {
-            var bounds = tbMap.map.createBounds();
-            tbMap.locations = [];
-            var dataMap = toLabelValueMap(data, datasources);
-            var currentDatasource = null;
-            var currentDatasourceIndex = -1;
-            var latIndex = -1;
-            var lngIndex = -1;
-
-            for (var i=0;i<data.length;i++) {
-                var dataKeyData = data[i];
-                var dataKey = dataKeyData.dataKey;
-                if (dataKeyData.datasource != currentDatasource) {
-                    currentDatasource = dataKeyData.datasource;
-                    currentDatasourceIndex++;
-                    latIndex = -1;
-                    lngIndex = -1;
-                }
-                var nameToCheck;
-                if (dataKey.locationAttrName) {
-                    nameToCheck = dataKey.locationAttrName;
-                } else {
-                    nameToCheck = dataKey.label;
-                }
-                if (nameToCheck === tbMap.locationSettings.latKeyName) {
-                    latIndex = i;
-                } else if (nameToCheck === tbMap.locationSettings.lngKeyName) {
-                    lngIndex = i;
-                }
-                if (latIndex > -1 && lngIndex > -1) {
-                    var location = {
-                        latIndex: latIndex,
-                        lngIndex: lngIndex,
-                        dsIndex: currentDatasourceIndex,
-                        settings: angular.copy(tbMap.locationSettings)
-                    };
-                    if (location.settings.showLabel) {
-                        location.settings.label = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.label);
-                        location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, currentDatasourceIndex);
-                        location.settings.labelText = location.settings.label;
-                    }
-                    if (location.settings.displayTooltip) {
-                        location.settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.tooltipPattern);
-                        location.settings.tooltipReplaceInfo = processPattern(location.settings.tooltipPattern, datasources, currentDatasourceIndex);
-                    }
-
-                    tbMap.locations.push(location);
-                    updateLocation(location, data, dataMap);
-                    if (location.polyline) {
-                        tbMap.map.extendBounds(bounds, location.polyline);
-                    } else if (location.marker) {
-                        tbMap.map.extendBoundsWithMarker(bounds, location.marker);
-                    }
-                    latIndex = -1;
-                    lngIndex = -1;
-                }
-
-            }
-            tbMap.map.fitBounds(bounds);
-        }
-
-        function updateLocations(data, datasources) {
-            var locationsChanged = false;
-            var bounds = tbMap.map.createBounds();
-            var dataMap = toLabelValueMap(data, datasources);
-            for (var p = 0; p < tbMap.locations.length; p++) {
-                var location = tbMap.locations[p];
-                locationsChanged |= updateLocation(location, data, dataMap);
-                if (location.polyline) {
-                    tbMap.map.extendBounds(bounds, location.polyline);
-                } else if (location.marker) {
-                    tbMap.map.extendBoundsWithMarker(bounds, location.marker);
-                }
-            }
-            if (locationsChanged) {
-                tbMap.map.fitBounds(bounds);
-            }
-        }
-
-        function createTooltipContent(tooltip, data, datasources) {
-            var content;
-            var settings = tooltip.locationSettings;
-            if (settings.useTooltipFunction && settings.tooltipFunction) {
-                var dataMap = toLabelValueMap(data, datasources);
-                try {
-                    settings.tooltipPattern = settings.tooltipFunction(dataMap.dataMap, dataMap.dsDataMap, tooltip.dsIndex);
-                } catch (e) {
-                    settings.tooltipPattern = null;
-                }
-                if (settings.tooltipPattern) {
-                    settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(datasources[tooltip.dsIndex], settings.tooltipPattern);
-                    settings.tooltipReplaceInfo = processPattern(settings.tooltipPattern, datasources, tooltip.dsIndex);
-                }
-            }
-            content = fillPattern(settings.tooltipPattern, settings.tooltipReplaceInfo, data);
-            return fillPatternWithActions(content, 'onTooltipAction', tooltip.markerArgs);
-        }
-
-        if (this.map && this.map.inited() && this.subscription) {
-            if (this.subscription.data) {
-                if (!this.locations) {
-                    loadLocations(this.subscription.data, this.subscription.datasources);
-                } else {
-                    updateLocations(this.subscription.data, this.subscription.datasources);
-                }
-                var tooltips = this.map.getTooltips();
-                for (var t = 0; t < tooltips.length; t++) {
-                    var tooltip = tooltips[t];
-                    var text = createTooltipContent(tooltip, this.subscription.data, this.subscription.datasources);
-                    tooltip.popup.setContent(text);
-                }
-            }
-        }
-    }
-
-    resize() {
-        if (this.map && this.map.inited()) {
-            this.map.invalidateSize();
-            if (this.locations && this.locations.length > 0) {
-                var bounds = this.map.createBounds();
-                for (var m = 0; m < this.markers.length; m++) {
-                    this.map.extendBoundsWithMarker(bounds, this.markers[m]);
-                }
-                if (this.polylines) {
-                    for (var p = 0; p < this.polylines.length; p++) {
-                        this.map.extendBounds(bounds, this.polylines[p]);
-                    }
-                }
-                this.map.fitBounds(bounds);
-            }
-        }
-    }
-
-    static settingsSchema(mapProvider, drawRoutes) {
-        var schema;
-        if (mapProvider === 'google-map') {
-            schema = angular.copy(googleMapSettingsSchema);
-        } else if (mapProvider === 'openstreet-map') {
-            schema = angular.copy(openstreetMapSettingsSchema);
-        } else if (mapProvider === 'image-map') {
-            return imageMapSettingsSchema;
-        } else if (mapProvider === 'tencent-map') {
-            schema = angular.copy(tencentMapSettingsSchema);
-        }
-        angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties);
-        schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required);
-        schema.form = schema.form.concat(commonMapSettingsSchema.form);
-        if (drawRoutes) {
-            angular.merge(schema.schema.properties, routeMapSettingsSchema.schema.properties);
-            schema.schema.required = schema.schema.required.concat(routeMapSettingsSchema.schema.required);
-            schema.form = schema.form.concat(routeMapSettingsSchema.form);
-        }
-        return schema;
-    }
-
-    static dataKeySettingsSchema(/*mapProvider*/) {
-        return {};
-    }
-
-    static actionSources() {
-        return {
-            'markerClick': {
-                name: 'widget-action.marker-click',
-                multiple: false
-            },
-            'tooltipAction': {
-                name: 'widget-action.tooltip-tag-action',
-                multiple: true
-            }
-        };
-    }
+
+	constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
+		var tbMap = this;
+		this.ctx = ctx;
+		this.mapProvider = mapProvider;
+		if (!$element) {
+			$element = ctx.$container;
+		}
+		this.utils = ctx.$scope.$injector.get('utils');
+		this.drawRoutes = drawRoutes;
+		this.markers = [];
+		this.polygons = [];
+		if (this.drawRoutes) {
+			this.polylines = [];
+		}
+
+		this.locationSettings = {};
+
+		var settings = ctx.settings;
+
+		this.callbacks = {};
+		this.callbacks.onLocationClick = function () {
+		};
+
+		if (settings.defaultZoomLevel) {
+			if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {
+				this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel);
+			}
+		}
+
+		this.dontFitMapBounds = settings.fitMapBounds === false;
+
+		if (!useDynamicLocations) {
+			this.subscription = this.ctx.defaultSubscription;
+		}
+
+		this.configureLocationsSettings();
+
+		var minZoomLevel = this.drawRoutes ? 18 : 15;
+
+
+		var initCallback = function () {
+			tbMap.update();
+			tbMap.resize();
+		};
+
+		this.ctx.$scope.onTooltipAction = function (event, actionName, dsIndex) {
+			tbMap.onTooltipAction(event, actionName, dsIndex);
+		};
+		this.tooltipActionsMap = {};
+		var descriptors = this.ctx.actionsApi.getActionDescriptors('tooltipAction');
+		descriptors.forEach(function (descriptor) {
+			tbMap.tooltipActionsMap[descriptor.name] = descriptor;
+		});
+
+		if (mapProvider === 'google-map') {
+			this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, 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);
+		} else if (mapProvider === 'image-map') {
+			this.map = new TbImageMap(this.ctx, $element, this.utils, initCallback,
+				settings.mapImageUrl,
+				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);
+		}
+
+
+		tbMap.initBounds = true;
+	}
+
+	setCallbacks(callbacks) {
+		Object.assign(this.callbacks, callbacks);
+	}
+
+	clearLocations() {
+		if (this.locations) {
+			var tbMap = this;
+			this.locations.forEach(function (location) {
+				if (location.marker) {
+					tbMap.map.removeMarker(location.marker);
+				}
+				if (location.polyline) {
+					tbMap.map.removePolyline(location.polyline);
+				}
+				if (location.polygon) {
+					tbMap.map.removePolygon(location.polygon);
+				}
+			});
+			this.locations = null;
+			this.markers = [];
+			this.polygons = [];
+			if (this.drawRoutes) {
+				this.polylines = [];
+			}
+		}
+	}
+
+	setSubscription(subscription) {
+		this.subscription = subscription;
+		this.clearLocations();
+	}
+
+	configureLocationsSettings() {
+
+		if (this.mapProvider == 'image-map') {
+			this.locationSettings.latKeyName = this.ctx.settings.xPosKeyName || 'xPos';
+			this.locationSettings.lngKeyName = this.ctx.settings.yPosKeyName || 'yPos';
+			this.locationSettings.markerOffsetX = angular.isDefined(this.ctx.settings.markerOffsetX) ? this.ctx.settings.markerOffsetX : 0.5;
+			this.locationSettings.markerOffsetY = angular.isDefined(this.ctx.settings.markerOffsetY) ? this.ctx.settings.markerOffsetY : 1;
+		} else {
+			this.locationSettings.latKeyName = this.ctx.settings.latKeyName || 'latitude';
+			this.locationSettings.lngKeyName = this.ctx.settings.lngKeyName || 'longitude';
+			this.locationSettings.polygonKeyName = this.ctx.settings.polygonKeyName || 'coordinates';
+		}
+
+		this.locationSettings.tooltipPattern = this.ctx.settings.tooltipPattern || "<b>${entityName}</b><br/><br/><b>Latitude:</b> ${" + this.locationSettings.latKeyName + ":7}<br/><b>Longitude:</b> ${" + this.locationSettings.lngKeyName + ":7}";
+
+		this.locationSettings.showLabel = this.ctx.settings.showLabel !== false;
+		this.locationSettings.displayTooltip = this.ctx.settings.showTooltip !== false;
+		this.locationSettings.autocloseTooltip = this.ctx.settings.autocloseTooltip !== false;
+		this.locationSettings.showPolygon = this.ctx.settings.showPolygon !== false;
+		this.locationSettings.labelColor = this.ctx.widgetConfig.color || '#000000';
+		this.locationSettings.label = this.ctx.settings.label || "${entityName}";
+		this.locationSettings.color = this.ctx.settings.color ? tinycolor(this.ctx.settings.color).toHexString() : "#FE7569";
+		this.locationSettings.polygonColor = this.ctx.settings.polygonColor ? tinycolor(this.ctx.settings.polygonColor).toHexString() : "#0000ff";
+		this.locationSettings.polygonStrokeColor = this.ctx.settings.polygonStrokeColor ? tinycolor(this.ctx.settings.polygonStrokeColor).toHexString() : "#fe0001";
+		this.locationSettings.polygonOpacity = angular.isDefined(this.ctx.settings.polygonOpacity) ? this.ctx.settings.polygonOpacity : 0.5;
+		this.locationSettings.polygonStrokeOpacity = angular.isDefined(this.ctx.settings.polygonStrokeOpacity) ? this.ctx.settings.polygonStrokeOpacity : 1;
+		this.locationSettings.polygonStrokeWeight = angular.isDefined(this.ctx.settings.polygonStrokeWeight) ? this.ctx.settings.polygonStrokeWeight : 1;
+
+		this.locationSettings.useLabelFunction = this.ctx.settings.useLabelFunction === true;
+		if (angular.isDefined(this.ctx.settings.labelFunction) && this.ctx.settings.labelFunction.length > 0) {
+			try {
+				this.locationSettings.labelFunction = new Function('data, dsData, dsIndex', this.ctx.settings.labelFunction);
+			} catch (e) {
+				this.locationSettings.labelFunction = null;
+			}
+		}
+
+		this.locationSettings.useTooltipFunction = this.ctx.settings.useTooltipFunction === true;
+		if (angular.isDefined(this.ctx.settings.tooltipFunction) && this.ctx.settings.tooltipFunction.length > 0) {
+			try {
+				this.locationSettings.tooltipFunction = new Function('data, dsData, dsIndex', this.ctx.settings.tooltipFunction);
+			} catch (e) {
+				this.locationSettings.tooltipFunction = null;
+			}
+		}
+
+		this.locationSettings.useColorFunction = this.ctx.settings.useColorFunction === true;
+		if (angular.isDefined(this.ctx.settings.colorFunction) && this.ctx.settings.colorFunction.length > 0) {
+			try {
+				this.locationSettings.colorFunction = new Function('data, dsData, dsIndex', this.ctx.settings.colorFunction);
+			} catch (e) {
+				this.locationSettings.colorFunction = null;
+			}
+		}
+		this.locationSettings.usePolygonColorFunction = this.ctx.settings.usePolygonColorFunction === true;
+		if (angular.isDefined(this.ctx.settings.polygonColorFunction) && this.ctx.settings.polygonColorFunction.length > 0) {
+			try {
+				this.locationSettings.polygonColorFunction = new Function('data, dsData, dsIndex', this.ctx.settings.polygonColorFunction);
+			} catch (e) {
+				this.locationSettings.polygonColorFunction = null;
+			}
+		}
+
+		this.locationSettings.useMarkerImageFunction = this.ctx.settings.useMarkerImageFunction === true;
+		if (angular.isDefined(this.ctx.settings.markerImageFunction) && this.ctx.settings.markerImageFunction.length > 0) {
+			try {
+				this.locationSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', this.ctx.settings.markerImageFunction);
+			} catch (e) {
+				this.locationSettings.markerImageFunction = null;
+			}
+		}
+
+		this.locationSettings.markerImages = this.ctx.settings.markerImages || [];
+
+		if (!this.locationSettings.useMarkerImageFunction &&
+			angular.isDefined(this.ctx.settings.markerImage) &&
+			this.ctx.settings.markerImage.length > 0) {
+			this.locationSettings.useMarkerImage = true;
+			var url = this.ctx.settings.markerImage;
+			var size = this.ctx.settings.markerImageSize || 34;
+			this.locationSettings.currentImage = {
+				url: url,
+				size: size
+			};
+		}
+
+		if (this.drawRoutes) {
+			this.locationSettings.strokeWeight = this.ctx.settings.strokeWeight || 2;
+			this.locationSettings.strokeOpacity = this.ctx.settings.strokeOpacity || 1.0;
+		}
+	}
+
+	onTooltipAction(event, actionName, dsIndex) {
+		var descriptor = this.tooltipActionsMap[actionName];
+		if (descriptor) {
+			var datasource = this.subscription.datasources[dsIndex];
+			var entityId = {};
+			entityId.id = datasource.entityId;
+			entityId.entityType = datasource.entityType;
+			var entityName = datasource.entityName;
+			this.ctx.actionsApi.handleWidgetAction(event, descriptor, entityId, entityName);
+		}
+	}
+
+	update() {
+
+		var tbMap = this;
+
+
+		function updateLocationLabel(location, dataMap) {
+			if (location.settings.showLabel) {
+				if (location.settings.useLabelFunction && location.settings.labelFunction) {
+					try {
+						location.settings.label = location.settings.labelFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
+					} catch (e) {
+						location.settings.label = null;
+					}
+					if (location.settings.label) {
+						var datasources = tbMap.subscription.datasources;
+						location.settings.label = tbMap.utils.createLabelFromDatasource(datasources[location.dsIndex], location.settings.label);
+						location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, location.dsIndex);
+						location.settings.labelText = location.settings.label;
+					}
+				}
+				if (location.settings.labelReplaceInfo.variables.length) {
+					location.settings.labelText = fillPattern(location.settings.label,
+						location.settings.labelReplaceInfo, tbMap.subscription.data);
+				}
+				tbMap.map.updateMarkerLabel(location.marker, location.settings);
+			}
+		}
+
+
+		function calculateLocationColor(location, dataMap) {
+			if (location.settings.useColorFunction && location.settings.colorFunction) {
+				var color;
+				try {
+					color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
+				} catch (e) {/**/
+				}
+				if (!color) {
+					color = '#FE7569';
+				}
+				return tinycolor(color).toHexString();
+			} else {
+				return location.settings.color;
+			}
+		}
+
+		function calculateLocationPolygonColor(location, dataMap) {
+			if (location.settings.usePolygonColorFunction && location.settings.polygonColorFunction) {
+				var color;
+				try {
+					color = location.settings.polygonColorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
+				} catch (e) {/**/
+				}
+				if (!color) {
+					color = '#007800';
+				}
+				return tinycolor(color).toHexString();
+			} else {
+				return location.settings.polygonColor;
+			}
+		}
+
+		function updateLocationColor(location, color, image) {
+			if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) {
+				if (!location.settings.useMarkerImage && !image) {
+					tbMap.map.updateMarkerColor(location.marker, color);
+				}
+				if (location.polyline) {
+					tbMap.map.updatePolylineColor(location.polyline, location.settings, color);
+				}
+				location.settings.calculatedColor = color;
+			}
+		}
+
+		function updateLocationPolygonColor(location, color) {
+			if (!location.settings.calculatedPolygonColor || location.settings.calculatedPolygonColor !== color) {
+				location.settings.calculatedPolygonColor = color;
+				if (location.polygon) {
+					tbMap.map.updatePolygonColor(location.polygon, location.settings, color);
+				}
+
+			}
+		}
+
+		function calculateLocationMarkerImage(location, dataMap) {
+			if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) {
+				var image = null;
+				try {
+					image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex);
+				} catch (e) {
+					image = null;
+				}
+				return image;
+			} else {
+				return null;
+			}
+		}
+
+		function updateLocationMarkerIcon(location, image) {
+			if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) {
+				location.settings.currentImage = image;
+				tbMap.map.updateMarkerIcon(location.marker, location.settings);
+			}
+		}
+
+		function updateLocationStyle(location, dataMap) {
+			updateLocationLabel(location, dataMap);
+			var color = calculateLocationColor(location, dataMap);
+			var polygonColor = calculateLocationPolygonColor(location, dataMap);
+			var image = calculateLocationMarkerImage(location, dataMap);
+			updateLocationColor(location, color, image);
+			if (location.settings.usePolygonColorFunction) updateLocationPolygonColor(location, polygonColor);
+			updateLocationMarkerIcon(location, image);
+		}
+
+		function createOrUpdateLocationMarker(location, markerLocation, dataMap) {
+			var changed = false;
+			if (!location.marker) {
+				var image = calculateLocationMarkerImage(location, dataMap);
+				if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) {
+					location.settings.currentImage = image;
+				}
+				location.marker = tbMap.map.createMarker(markerLocation, location.dsIndex, location.settings,
+					function (event) {
+						tbMap.callbacks.onLocationClick(location);
+						locationRowClick(event, location);
+					}, [location.dsIndex]);
+				tbMap.markers.push(location.marker);
+				changed = true;
+			} else {
+				var prevPosition = tbMap.map.getMarkerPosition(location.marker);
+				if (!prevPosition.equals(markerLocation)) {
+					tbMap.map.setMarkerPosition(location.marker, markerLocation);
+					changed = true;
+				}
+			}
+			return changed;
+		}
+
+		function locationRowClick($event, location) {
+			var descriptors = tbMap.ctx.actionsApi.getActionDescriptors('markerClick');
+			if (descriptors.length) {
+				var datasource = tbMap.subscription.datasources[location.dsIndex];
+				var entityId = {};
+				entityId.id = datasource.entityId;
+				entityId.entityType = datasource.entityType;
+				var entityName = datasource.entityName;
+				tbMap.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName);
+			}
+		}
+
+		function updateLocation(location, data, dataMap) {
+			var locationChanged = false;
+			if (location.latIndex > -1 && location.lngIndex > -1) {
+				var latData = data[location.latIndex].data;
+				var lngData = data[location.lngIndex].data;
+				var lat, lng, latLng;
+				if (latData.length > 0 && lngData.length > 0) {
+					if (tbMap.drawRoutes) {
+						// Create or update route
+						var latLngs = [];
+						for (var i = 0; i < latData.length; i++) {
+							lat = latData[i][1];
+							lng = lngData[i][1];
+							if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) {
+								latLng = tbMap.map.createLatLng(lat, lng);
+								if (i == 0 || !latLngs[latLngs.length - 1].equals(latLng)) {
+									latLngs.push(latLng);
+								}
+							}
+						}
+						if (latLngs.length > 0) {
+							var markerLocation = latLngs[latLngs.length - 1];
+							createOrUpdateLocationMarker(location, markerLocation, dataMap);
+						}
+						if (!location.polyline) {
+							location.polyline = tbMap.map.createPolyline(latLngs, location.settings);
+							tbMap.polylines.push(location.polyline);
+							locationChanged = true;
+						} else {
+							var prevPath = tbMap.map.getPolylineLatLngs(location.polyline);
+							if (!prevPath || !arraysEqual(prevPath, latLngs)) {
+								tbMap.map.setPolylineLatLngs(location.polyline, latLngs);
+								locationChanged = true;
+							}
+						}
+					} else {
+						// Create or update marker
+						lat = latData[latData.length - 1][1];
+						lng = lngData[lngData.length - 1][1];
+						if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) {
+							latLng = tbMap.map.createLatLng(lat, lng);
+							if (createOrUpdateLocationMarker(location, latLng, dataMap)) {
+								locationChanged = true;
+							}
+						}
+
+
+						if (location.settings.showPolygon && dataMap.dsDataMap[location.dsIndex][location.settings.polygonKeyName] !== null) {
+							let polygonLatLngsRaw = angular.fromJson(dataMap.dsDataMap[location.dsIndex][location.settings.polygonKeyName]);
+							let polygonLatLngs = !polygonLatLngsRaw || mapPolygonArray(polygonLatLngsRaw);
+							if (!location.polygon && polygonLatLngs.length > 0) {
+								location.polygon = tbMap.map.createPolygon(polygonLatLngs, location.settings);
+								tbMap.polygons.push(location.polygon);
+							} else if (polygonLatLngs.length > 0) {
+								let prevPolygonArr = tbMap.map.getPolygonLatLngs(location.polygon);
+								if (!prevPolygonArr || !arraysEqual(prevPolygonArr, polygonLatLngs)) {
+									tbMap.map.setPolygonLatLngs(location.polygon, polygonLatLngs);
+								}
+							}
+						}
+					}
+					if (location.marker) {
+						updateLocationStyle(location, dataMap);
+					}
+				}
+			}
+			return locationChanged;
+		}
+
+		function loadLocations(data, datasources) {
+			var bounds = tbMap.map.createBounds();
+			tbMap.locations = [];
+			var dataMap = toLabelValueMap(data, datasources);
+			var currentDatasource = null;
+			var currentDatasourceIndex = -1;
+			var latIndex = -1;
+			var lngIndex = -1;
+
+			for (var i = 0; i < data.length; i++) {
+				var dataKeyData = data[i];
+				var dataKey = dataKeyData.dataKey;
+				if (dataKeyData.datasource != currentDatasource) {
+					currentDatasource = dataKeyData.datasource;
+					currentDatasourceIndex++;
+					latIndex = -1;
+					lngIndex = -1;
+				}
+				var nameToCheck;
+				if (dataKey.locationAttrName) {
+					nameToCheck = dataKey.locationAttrName;
+				} else {
+					nameToCheck = dataKey.label;
+				}
+				if (nameToCheck === tbMap.locationSettings.latKeyName) {
+					latIndex = i;
+				} else if (nameToCheck === tbMap.locationSettings.lngKeyName) {
+					lngIndex = i;
+				}
+
+				if (latIndex > -1 && lngIndex > -1) {
+					var location = {
+						latIndex: latIndex,
+						lngIndex: lngIndex,
+						dsIndex: currentDatasourceIndex,
+						settings: angular.copy(tbMap.locationSettings)
+					};
+					if (location.settings.showLabel) {
+						location.settings.label = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.label);
+						location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, currentDatasourceIndex);
+						location.settings.labelText = location.settings.label;
+					}
+					if (location.settings.displayTooltip) {
+						location.settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.tooltipPattern);
+						location.settings.tooltipReplaceInfo = processPattern(location.settings.tooltipPattern, datasources, currentDatasourceIndex);
+					}
+					tbMap.locations.push(location);
+					updateLocation(location, data, dataMap);
+					if (location.polyline) {
+						tbMap.map.extendBounds(bounds, location.polyline);
+					} else if (location.marker) {
+						tbMap.map.extendBoundsWithMarker(bounds, location.marker);
+					}
+					latIndex = -1;
+					lngIndex = -1;
+				}
+
+			}
+			tbMap.map.fitBounds(bounds);
+		}
+
+		function mapPolygonArray (rawArray) {
+			let latLngArray = rawArray.map(function (el) {
+				if (el.length === 2) {
+					return tbMap.map.createLatLng(el[0], el[1]);
+				} else if (el.length > 2) {
+					return mapPolygonArray(el);
+				} else {
+					return tbMap.map.createLatLng(false);
+				}
+			});
+			return latLngArray;
+		}
+
+		function updateLocations(data, datasources) {
+			var locationsChanged = false;
+			var bounds = tbMap.map.createBounds();
+			var dataMap = toLabelValueMap(data, datasources);
+			for (var p = 0; p < tbMap.locations.length; p++) {
+				var location = tbMap.locations[p];
+				locationsChanged |= updateLocation(location, data, dataMap);
+				if (location.polyline) {
+					tbMap.map.extendBounds(bounds, location.polyline);
+				} else if (location.marker) {
+					tbMap.map.extendBoundsWithMarker(bounds, location.marker);
+				}
+			}
+			if (locationsChanged && tbMap.initBounds) {
+				tbMap.initBounds = !datasources.every(
+					function (ds) {
+						return ds.dataReceived === true;
+					});
+				tbMap.map.fitBounds(bounds);
+			}
+		}
+
+		function createTooltipContent(tooltip, data, datasources) {
+			var content;
+			var settings = tooltip.locationSettings;
+			if (settings.useTooltipFunction && settings.tooltipFunction) {
+				var dataMap = toLabelValueMap(data, datasources);
+				try {
+					settings.tooltipPattern = settings.tooltipFunction(dataMap.dataMap, dataMap.dsDataMap, tooltip.dsIndex);
+				} catch (e) {
+					settings.tooltipPattern = null;
+				}
+				if (settings.tooltipPattern) {
+					settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(datasources[tooltip.dsIndex], settings.tooltipPattern);
+					settings.tooltipReplaceInfo = processPattern(settings.tooltipPattern, datasources, tooltip.dsIndex);
+				}
+			}
+			content = fillPattern(settings.tooltipPattern, settings.tooltipReplaceInfo, data);
+			return fillPatternWithActions(content, 'onTooltipAction', tooltip.markerArgs);
+		}
+
+		if (this.map && this.map.inited() && this.subscription) {
+			if (this.subscription.data) {
+				if (!this.locations) {
+					loadLocations(this.subscription.data, this.subscription.datasources);
+				} else {
+					updateLocations(this.subscription.data, this.subscription.datasources);
+				}
+				var tooltips = this.map.getTooltips();
+				for (var t = 0; t < tooltips.length; t++) {
+					var tooltip = tooltips[t];
+					var text = createTooltipContent(tooltip, this.subscription.data, this.subscription.datasources);
+					tooltip.popup.setContent(text);
+				}
+			}
+		}
+
+	}
+
+	resize() {
+		if (this.map && this.map.inited()) {
+			this.map.invalidateSize();
+			if (this.locations && this.locations.length > 0) {
+				var bounds = this.map.createBounds();
+				for (var m = 0; m < this.markers.length; m++) {
+					this.map.extendBoundsWithMarker(bounds, this.markers[m]);
+				}
+				if (this.polylines) {
+					for (var p = 0; p < this.polylines.length; p++) {
+						this.map.extendBounds(bounds, this.polylines[p]);
+					}
+				}
+				this.map.fitBounds(bounds);
+			}
+		}
+	}
+
+	static settingsSchema(mapProvider, drawRoutes) {
+		var schema;
+		if (mapProvider === 'google-map') {
+			schema = angular.copy(googleMapSettingsSchema);
+		} else if (mapProvider === 'openstreet-map') {
+			schema = angular.copy(openstreetMapSettingsSchema);
+		} else if (mapProvider === 'image-map') {
+			return imageMapSettingsSchema;
+		} else if (mapProvider === 'tencent-map') {
+			schema = angular.copy(tencentMapSettingsSchema);
+		}
+		angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties);
+		schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required);
+		schema.form = schema.form.concat(commonMapSettingsSchema.form);
+		if (drawRoutes) {
+			angular.merge(schema.schema.properties, routeMapSettingsSchema.schema.properties);
+			schema.schema.required = schema.schema.required.concat(routeMapSettingsSchema.schema.required);
+			schema.form = schema.form.concat(routeMapSettingsSchema.form);
+		}
+		return schema;
+	}
+
+	static dataKeySettingsSchema(/*mapProvider*/) {
+		return {};
+	}
+
+	static actionSources() {
+		return {
+			'markerClick': {
+				name: 'widget-action.marker-click',
+				multiple: false
+			},
+			'tooltipAction': {
+				name: 'widget-action.tooltip-tag-action',
+				multiple: true
+			}
+		};
+	}
 
 }
 
 const googleMapSettingsSchema =
-    {
-        "schema":{
-            "title":"Google Map Configuration",
-            "type":"object",
-            "properties":{
-                "gmApiKey":{
-                    "title":"Google Maps API Key",
-                    "type":"string"
-                },
-                "gmDefaultMapType":{
-                    "title":"Default map type",
-                    "type":"string",
-                    "default":"roadmap"
-                }
-            },
-            "required":[
-                "gmApiKey"
-            ]
-        },
-        "form":[
-            "gmApiKey",
-            {
-                "key":"gmDefaultMapType",
-                "type":"rc-select",
-                "multiple":false,
-                "items":[
-                    {
-                        "value":"roadmap",
-                        "label":"Roadmap"
-                    },
-                    {
-                        "value":"satellite",
-                        "label":"Satellite"
-                    },
-                    {
-                        "value":"hybrid",
-                        "label":"Hybrid"
-                    },
-                    {
-                        "value":"terrain",
-                        "label":"Terrain"
-                    }
-                ]
-            }
-        ]
-    };
-    
+	{
+		"schema": {
+			"title": "Google Map Configuration",
+			"type": "object",
+			"properties": {
+				"gmApiKey": {
+					"title": "Google Maps API Key",
+					"type": "string"
+				},
+				"gmDefaultMapType": {
+					"title": "Default map type",
+					"type": "string",
+					"default": "roadmap"
+				}
+			},
+			"required": [
+				"gmApiKey"
+			]
+		},
+		"form": [
+			"gmApiKey",
+			{
+				"key": "gmDefaultMapType",
+				"type": "rc-select",
+				"multiple": false,
+				"items": [
+					{
+						"value": "roadmap",
+						"label": "Roadmap"
+					},
+					{
+						"value": "satellite",
+						"label": "Satellite"
+					},
+					{
+						"value": "hybrid",
+						"label": "Hybrid"
+					},
+					{
+						"value": "terrain",
+						"label": "Terrain"
+					}
+				]
+			}
+		]
+	};
+
 const tencentMapSettingsSchema =
-    {
-        "schema":{
-            "title":"Tencent Map Configuration",
-            "type":"object",
-            "properties":{
-                "tmApiKey":{
-                    "title":"Tencent Maps API Key",
-                    "type":"string"
-                },
-                "tmDefaultMapType":{
-                    "title":"Default map type",
-                    "type":"string",
-                    "default":"roadmap"
-                }
-            },
-            "required":[
-                "tmApiKey"
-            ]
-        },
-        "form":[
-            "tmApiKey",
-            {
-                "key":"tmDefaultMapType",
-                "type":"rc-select",
-                "multiple":false,
-                "items":[
-                    {
-                        "value":"roadmap",
-                        "label":"Roadmap"
-                    },
-                    {
-                        "value":"satellite",
-                        "label":"Satellite"
-                    },
-                    {
-                        "value":"hybrid",
-                        "label":"Hybrid"
-                    },
-                ]
-            }
-        ]
-    };
-    
+	{
+		"schema": {
+			"title": "Tencent Map Configuration",
+			"type": "object",
+			"properties": {
+				"tmApiKey": {
+					"title": "Tencent Maps API Key",
+					"type": "string"
+				},
+				"tmDefaultMapType": {
+					"title": "Default map type",
+					"type": "string",
+					"default": "roadmap"
+				}
+			},
+			"required": [
+				"tmApiKey"
+			]
+		},
+		"form": [
+			"tmApiKey",
+			{
+				"key": "tmDefaultMapType",
+				"type": "rc-select",
+				"multiple": false,
+				"items": [
+					{
+						"value": "roadmap",
+						"label": "Roadmap"
+					},
+					{
+						"value": "satellite",
+						"label": "Satellite"
+					},
+					{
+						"value": "hybrid",
+						"label": "Hybrid"
+					},
+				]
+			}
+		]
+	};
+
 const openstreetMapSettingsSchema =
-    {
-        "schema":{
-            "title":"Openstreet Map Configuration",
-            "type":"object",
-            "properties":{
-                "mapProvider":{
-                    "title":"Map provider",
-                    "type":"string",
-                    "default":"OpenStreetMap.Mapnik"
-                }
-            },
-            "required":[
-            ]
-        },
-        "form":[
-            {
-                "key":"mapProvider",
-                "type":"rc-select",
-                "multiple":false,
-                "items":[
-                    {
-                        "value":"OpenStreetMap.Mapnik",
-                        "label":"OpenStreetMap.Mapnik (Default)"
-                    },
-                    {
-                        "value":"OpenStreetMap.BlackAndWhite",
-                        "label":"OpenStreetMap.BlackAndWhite"
-                    },
-                    {
-                        "value":"OpenStreetMap.HOT",
-                        "label":"OpenStreetMap.HOT"
-                    },
-                    {
-                        "value":"Esri.WorldStreetMap",
-                        "label":"Esri.WorldStreetMap"
-                    },
-                    {
-                        "value":"Esri.WorldTopoMap",
-                        "label":"Esri.WorldTopoMap"
-                    },
-                    {
-                        "value":"CartoDB.Positron",
-                        "label":"CartoDB.Positron"
-                    },
-                    {
-                        "value":"CartoDB.DarkMatter",
-                        "label":"CartoDB.DarkMatter"
-                    }
-                ]
-            }
-        ]
-    };
+	{
+		"schema": {
+			"title": "Openstreet Map Configuration",
+			"type": "object",
+			"properties": {
+				"mapProvider": {
+					"title": "Map provider",
+					"type": "string",
+					"default": "OpenStreetMap.Mapnik"
+				}
+			},
+			"required": []
+		},
+		"form": [
+			{
+				"key": "mapProvider",
+				"type": "rc-select",
+				"multiple": false,
+				"items": [
+					{
+						"value": "OpenStreetMap.Mapnik",
+						"label": "OpenStreetMap.Mapnik (Default)"
+					},
+					{
+						"value": "OpenStreetMap.BlackAndWhite",
+						"label": "OpenStreetMap.BlackAndWhite"
+					},
+					{
+						"value": "OpenStreetMap.HOT",
+						"label": "OpenStreetMap.HOT"
+					},
+					{
+						"value": "Esri.WorldStreetMap",
+						"label": "Esri.WorldStreetMap"
+					},
+					{
+						"value": "Esri.WorldTopoMap",
+						"label": "Esri.WorldTopoMap"
+					},
+					{
+						"value": "CartoDB.Positron",
+						"label": "CartoDB.Positron"
+					},
+					{
+						"value": "CartoDB.DarkMatter",
+						"label": "CartoDB.DarkMatter"
+					}
+				]
+			}
+		]
+	};
 
 const commonMapSettingsSchema =
-    {
-        "schema":{
-            "title":"Map Configuration",
-            "type":"object",
-            "properties":{
-                "defaultZoomLevel":{
-                    "title":"Default map zoom level (1 - 20)",
-                    "type":"number"
-                },
-                "fitMapBounds":{
-                    "title":"Fit map bounds to cover all markers",
-                    "type":"boolean",
-                    "default":true
-                },
-                "latKeyName":{
-                    "title":"Latitude key name",
-                    "type":"string",
-                    "default":"latitude"
-                },
-                "lngKeyName":{
-                    "title":"Longitude key name",
-                    "type":"string",
-                    "default":"longitude"
-                },
-                "showLabel":{
-                    "title":"Show label",
-                    "type":"boolean",
-                    "default":true
-                },
-                "label":{
-                    "title":"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
-                    "type":"string",
-                    "default":"${entityName}"
-                },
-                "useLabelFunction": {
-                    "title":"Use label function",
-                    "type":"boolean",
-                    "default":false
-                },
-                "labelFunction":{
-                    "title":"Label function: f(data, dsData, dsIndex)",
-                    "type":"string"
-                },
-                "showTooltip": {
-                    "title": "Show tooltip",
-                    "type":"boolean",
-                    "default":true
-                },
-                "autocloseTooltip": {
-                    "title": "Auto-close tooltips",
-                    "type":"boolean",
-                    "default":true
-                },
-                "tooltipPattern":{
-                    "title":"Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
-                    "type":"string",
-                    "default":"<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}"
-                },
-                "useTooltipFunction": {
-                    "title":"Use tooltip function",
-                    "type":"boolean",
-                    "default":false
-                },
-                "tooltipFunction":{
-                    "title":"Tooltip function: f(data, dsData, dsIndex)",
-                    "type":"string"
-                },
-                "color":{
-                    "title":"Color",
-                    "type":"string"
-                },
-                "useColorFunction":{
-                    "title":"Use color function",
-                    "type":"boolean",
-                    "default":false
-                },
-                "colorFunction":{
-                    "title":"Color function: f(data, dsData, dsIndex)",
-                    "type":"string"
-                },
-                "markerImage":{
-                    "title":"Custom marker image",
-                    "type":"string"
-                },
-                "markerImageSize":{
-                    "title":"Custom marker image size (px)",
-                    "type":"number",
-                    "default":34
-                },
-                "useMarkerImageFunction":{
-                    "title":"Use marker image function",
-                    "type":"boolean",
-                    "default":false
-                },
-                "markerImageFunction":{
-                    "title":"Marker image function: f(data, images, dsData, dsIndex)",
-                    "type":"string"
-                },
-                "markerImages":{
-                    "title":"Marker images",
-                    "type":"array",
-                    "items":{
-                        "title":"Marker image",
-                        "type":"string"
-                    }
-                }
-            },
-            "required":[]
-        },
-        "form":[
-            "defaultZoomLevel",
-            "fitMapBounds",
-            "latKeyName",
-            "lngKeyName",
-            "showLabel",
-            "label",
-            "useLabelFunction",
-            {
-                "key":"labelFunction",
-                "type":"javascript"
-            },
-            "showTooltip",
-            "autocloseTooltip",
-            {
-                "key": "tooltipPattern",
-                "type": "textarea"
-            },
-            "useTooltipFunction",
-            {
-                "key":"tooltipFunction",
-                "type":"javascript"
-            },
-            {
-                "key":"color",
-                "type":"color"
-            },
-            "useColorFunction",
-            {
-                "key":"colorFunction",
-                "type":"javascript"
-            },
-            {
-                "key":"markerImage",
-                "type":"image"
-            },
-            "markerImageSize",
-            "useMarkerImageFunction",
-            {
-                "key":"markerImageFunction",
-                "type":"javascript"
-            },
-            {
-                "key":"markerImages",
-                "items":[
-                    {
-                        "key":"markerImages[]",
-                        "type":"image"
-                    }
-                ]
-            }
-        ]
-};
+	{
+		"schema": {
+			"title": "Map Configuration",
+			"type": "object",
+			"properties": {
+				"defaultZoomLevel": {
+					"title": "Default map zoom level (1 - 20)",
+					"type": "number"
+				},
+				"fitMapBounds": {
+					"title": "Fit map bounds to cover all markers",
+					"type": "boolean",
+					"default": true
+				},
+				"latKeyName": {
+					"title": "Latitude key name",
+					"type": "string",
+					"default": "latitude"
+				},
+				"lngKeyName": {
+					"title": "Longitude key name",
+					"type": "string",
+					"default": "longitude"
+				},
+				"showLabel": {
+					"title": "Show label",
+					"type": "boolean",
+					"default": true
+				},
+				"label": {
+					"title": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
+					"type": "string",
+					"default": "${entityName}"
+				},
+				"useLabelFunction": {
+					"title": "Use label function",
+					"type": "boolean",
+					"default": false
+				},
+				"labelFunction": {
+					"title": "Label function: f(data, dsData, dsIndex)",
+					"type": "string"
+				},
+				"showTooltip": {
+					"title": "Show tooltip",
+					"type": "boolean",
+					"default": true
+				},
+				"autocloseTooltip": {
+					"title": "Auto-close tooltips",
+					"type": "boolean",
+					"default": true
+				},
+				"tooltipPattern": {
+					"title": "Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
+					"type": "string",
+					"default": "<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}"
+				},
+				"useTooltipFunction": {
+					"title": "Use tooltip function",
+					"type": "boolean",
+					"default": false
+				},
+				"tooltipFunction": {
+					"title": "Tooltip function: f(data, dsData, dsIndex)",
+					"type": "string"
+				},
+				"color": {
+					"title": "Color",
+					"type": "string"
+				},
+				"useColorFunction": {
+					"title": "Use color function",
+					"type": "boolean",
+					"default": false
+				},
+				"colorFunction": {
+					"title": "Color function: f(data, dsData, dsIndex)",
+					"type": "string"
+				},
+				"showPolygon": {
+					"title": "Show polygon",
+					"type": "boolean",
+					"default": false
+				},
+				"polygonKeyName": {
+					"title": "Polygon key name",
+					"type": "string",
+					"default": "coordinates"
+				},
+				"polygonColor": {
+					"title": "Polygon color",
+					"type": "string"
+				},
+				"polygonOpacity": {
+					"title": "Polygon opacity",
+					"type": "number",
+					"default": 0.5
+				},
+				"polygonStrokeColor": {
+					"title": "Stroke color",
+					"type": "string"
+				},
+				"polygonStrokeOpacity": {
+					"title": "Stroke opacity",
+					"type": "number",
+					"default": 1
+				},
+				"polygonStrokeWeight": {
+					"title": "Stroke weight",
+					"type": "number",
+					"default": 1
+				},
+				"usePolygonColorFunction": {
+					"title": "Use polygon color function",
+					"type": "boolean",
+					"default": false
+				},
+				"polygonColorFunction": {
+					"title": "Polygon Color function: f(data, dsData, dsIndex)",
+					"type": "string"
+				},
+				"markerImage": {
+					"title": "Custom marker image",
+					"type": "string"
+				},
+				"markerImageSize": {
+					"title": "Custom marker image size (px)",
+					"type": "number",
+					"default": 34
+				},
+				"useMarkerImageFunction": {
+					"title": "Use marker image function",
+					"type": "boolean",
+					"default": false
+				},
+				"markerImageFunction": {
+					"title": "Marker image function: f(data, images, dsData, dsIndex)",
+					"type": "string"
+				},
+				"markerImages": {
+					"title": "Marker images",
+					"type": "array",
+					"items": {
+						"title": "Marker image",
+						"type": "string"
+					}
+				}
+			},
+			"required": []
+		},
+		"form": [
+			"defaultZoomLevel",
+			"fitMapBounds",
+			"latKeyName",
+			"lngKeyName",
+			"showLabel",
+			"label",
+			"useLabelFunction",
+			{
+				"key": "labelFunction",
+				"type": "javascript"
+			},
+			"showTooltip",
+			"autocloseTooltip",
+			{
+				"key": "tooltipPattern",
+				"type": "textarea"
+			},
+			"useTooltipFunction",
+			{
+				"key": "tooltipFunction",
+				"type": "javascript"
+			},
+			{
+				"key": "color",
+				"type": "color"
+			},
+			"useColorFunction",
+			{
+				"key": "colorFunction",
+				"type": "javascript"
+			}, "showPolygon", "polygonKeyName",
+			{
+				"key": "polygonColor",
+				"type": "color"
+			},
+			"polygonOpacity",
+			{
+				"key": "polygonStrokeColor",
+				"type": "color"
+			},
+			"polygonStrokeOpacity","polygonStrokeWeight","usePolygonColorFunction",
+			{
+				"key": "polygonColorFunction",
+				"type": "javascript"
+			},
+			{
+				"key": "markerImage",
+				"type": "image"
+			},
+			"markerImageSize",
+			"useMarkerImageFunction",
+			{
+				"key": "markerImageFunction",
+				"type": "javascript"
+			},
+			{
+				"key": "markerImages",
+				"items": [
+					{
+						"key": "markerImages[]",
+						"type": "image"
+					}
+				]
+			}
+		]
+	};
 
 const routeMapSettingsSchema =
-    {
-        "schema":{
-            "title":"Route Map Configuration",
-            "type":"object",
-            "properties":{
-                "strokeWeight": {
-                    "title": "Stroke weight",
-                    "type": "number",
-                    "default": 2
-                },
-                "strokeOpacity": {
-                    "title": "Stroke opacity",
-                    "type": "number",
-                    "default": 1.0
-                }
-            },
-            "required":[
-            ]
-        },
-        "form":[
-            "strokeWeight",
-            "strokeOpacity"
-        ]
-    };
+	{
+		"schema": {
+			"title": "Route Map Configuration",
+			"type": "object",
+			"properties": {
+				"strokeWeight": {
+					"title": "Stroke weight",
+					"type": "number",
+					"default": 2
+				},
+				"strokeOpacity": {
+					"title": "Stroke opacity",
+					"type": "number",
+					"default": 1.0
+				}
+			},
+			"required": []
+		},
+		"form": [
+			"strokeWeight",
+			"strokeOpacity"
+		]
+	};
 
 const imageMapSettingsSchema =
-{
-    "schema":{
-        "title":"Image Map Configuration",
-        "type":"object",
-        "properties":{
-            "mapImageUrl": {
-                "title": "Image map background",
-                "type": "string",
-                "default": "data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=="
-            },
-            "imageEntityAlias": {
-                "title": "Image URL source entity alias",
-                "type": "string",
-                "default": ""
-            },
-            "imageUrlAttribute": {
-                "title": "Image URL source entity attribute",
-                "type": "string",
-                "default": ""
-            },
-            "xPosKeyName":{
-                "title":"X position key name",
-                "type":"string",
-                "default":"xPos"
-            },
-            "yPosKeyName":{
-                "title":"Y position key name",
-                "type":"string",
-                "default":"yPos"
-            },
-            "showLabel":{
-                "title":"Show label",
-                "type":"boolean",
-                "default":true
-            },
-            "label":{
-                "title":"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
-                "type":"string",
-                "default":"${entityName}"
-            },
-            "useLabelFunction": {
-                "title":"Use label function",
-                "type":"boolean",
-                "default":false
-            },
-            "labelFunction":{
-                "title":"Label function: f(data, dsData, dsIndex)",
-                "type":"string"
-            },
-            "showTooltip": {
-                "title": "Show tooltip",
-                "type":"boolean",
-                "default":true
-            },
-            "autocloseTooltip": {
-                "title": "Auto-close tooltips",
-                "type":"boolean",
-                "default":true
-            },
-            "tooltipPattern":{
-                "title":"Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
-                "type":"string",
-                "default":"<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}"
-            },
-            "useTooltipFunction": {
-                "title":"Use tooltip function",
-                "type":"boolean",
-                "default":false
-            },
-            "tooltipFunction":{
-                "title":"Tooltip function: f(data, dsData, dsIndex)",
-                "type":"string"
-            },
-            "color":{
-                "title":"Color",
-                "type":"string"
-            },
-            "posFunction":{
-                "title":"Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each",
-                "type":"string",
-                "default": "return {x: origXPos, y: origYPos};"
-            },
-            "markerOffsetX": {
-                "title": "Marker X offset relative to position",
-                "type": "number",
-                "default": 0.5
-            },
-            "markerOffsetY": {
-                "title": "Marker Y offset relative to position",
-                "type": "number",
-                "default": 1
-            },
-            "useColorFunction":{
-                "title":"Use color function",
-                "type":"boolean",
-                "default":false
-            },
-            "colorFunction":{
-                "title":"Color function: f(data, dsData, dsIndex)",
-                "type":"string"
-            },
-            "markerImage":{
-                "title":"Custom marker image",
-                "type":"string"
-            },
-            "markerImageSize":{
-                "title":"Custom marker image size (px)",
-                "type":"number",
-                "default":34
-            },
-            "useMarkerImageFunction":{
-                "title":"Use marker image function",
-                "type":"boolean",
-                "default":false
-            },
-            "markerImageFunction":{
-                "title":"Marker image function: f(data, images, dsData, dsIndex)",
-                "type":"string"
-            },
-            "markerImages":{
-                "title":"Marker images",
-                "type":"array",
-                "items":{
-                    "title":"Marker image",
-                    "type":"string"
-                }
-            }
-        },
-        "required":[]
-    },
-    "form":[
-        {
-            "key": "mapImageUrl",
-            "type": "image"
-        },
-        "imageEntityAlias",
-        "imageUrlAttribute",
-        "xPosKeyName",
-        "yPosKeyName",
-        "showLabel",
-        "label",
-        "useLabelFunction",
-        {
-            "key":"labelFunction",
-            "type":"javascript"
-        },
-        "showTooltip",
-        "autocloseTooltip",
-        {
-            "key": "tooltipPattern",
-            "type": "textarea"
-        },
-        "useTooltipFunction",
-        {
-            "key":"tooltipFunction",
-            "type":"javascript"
-        },
-        {
-            "key":"color",
-            "type":"color"
-        },
-        {
-            "key":"posFunction",
-            "type":"javascript"
-        },
-        "markerOffsetX",
-        "markerOffsetY",
-        "useColorFunction",
-        {
-            "key":"colorFunction",
-            "type":"javascript"
-        },
-        {
-            "key":"markerImage",
-            "type":"image"
-        },
-        "markerImageSize",
-        "useMarkerImageFunction",
-        {
-            "key":"markerImageFunction",
-            "type":"javascript"
-        },
-        {
-            "key":"markerImages",
-            "items":[
-                {
-                    "key":"markerImages[]",
-                    "type":"image"
-                }
-            ]
-        }
-    ]
-};
\ No newline at end of file
+	{
+		"schema": {
+			"title": "Image Map Configuration",
+			"type": "object",
+			"properties": {
+				"mapImageUrl": {
+					"title": "Image map background",
+					"type": "string",
+					"default": "data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=="
+				},
+				"imageEntityAlias": {
+					"title": "Image URL source entity alias",
+					"type": "string",
+					"default": ""
+				},
+				"imageUrlAttribute": {
+					"title": "Image URL source entity attribute",
+					"type": "string",
+					"default": ""
+				},
+				"xPosKeyName": {
+					"title": "X position key name",
+					"type": "string",
+					"default": "xPos"
+				},
+				"yPosKeyName": {
+					"title": "Y position key name",
+					"type": "string",
+					"default": "yPos"
+				},
+				"showLabel": {
+					"title": "Show label",
+					"type": "boolean",
+					"default": true
+				},
+				"label": {
+					"title": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
+					"type": "string",
+					"default": "${entityName}"
+				},
+				"useLabelFunction": {
+					"title": "Use label function",
+					"type": "boolean",
+					"default": false
+				},
+				"labelFunction": {
+					"title": "Label function: f(data, dsData, dsIndex)",
+					"type": "string"
+				},
+				"showTooltip": {
+					"title": "Show tooltip",
+					"type": "boolean",
+					"default": true
+				},
+				"autocloseTooltip": {
+					"title": "Auto-close tooltips",
+					"type": "boolean",
+					"default": true
+				},
+				"tooltipPattern": {
+					"title": "Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
+					"type": "string",
+					"default": "<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}"
+				},
+				"useTooltipFunction": {
+					"title": "Use tooltip function",
+					"type": "boolean",
+					"default": false
+				},
+				"tooltipFunction": {
+					"title": "Tooltip function: f(data, dsData, dsIndex)",
+					"type": "string"
+				},
+				"color": {
+					"title": "Color",
+					"type": "string"
+				},
+				"posFunction": {
+					"title": "Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each",
+					"type": "string",
+					"default": "return {x: origXPos, y: origYPos};"
+				},
+				"markerOffsetX": {
+					"title": "Marker X offset relative to position",
+					"type": "number",
+					"default": 0.5
+				},
+				"markerOffsetY": {
+					"title": "Marker Y offset relative to position",
+					"type": "number",
+					"default": 1
+				},
+				"useColorFunction": {
+					"title": "Use color function",
+					"type": "boolean",
+					"default": false
+				},
+				"colorFunction": {
+					"title": "Color function: f(data, dsData, dsIndex)",
+					"type": "string"
+				},
+				"markerImage": {
+					"title": "Custom marker image",
+					"type": "string"
+				},
+				"markerImageSize": {
+					"title": "Custom marker image size (px)",
+					"type": "number",
+					"default": 34
+				},
+				"useMarkerImageFunction": {
+					"title": "Use marker image function",
+					"type": "boolean",
+					"default": false
+				},
+				"markerImageFunction": {
+					"title": "Marker image function: f(data, images, dsData, dsIndex)",
+					"type": "string"
+				},
+				"markerImages": {
+					"title": "Marker images",
+					"type": "array",
+					"items": {
+						"title": "Marker image",
+						"type": "string"
+					}
+				}
+			},
+			"required": []
+		},
+		"form": [
+			{
+				"key": "mapImageUrl",
+				"type": "image"
+			},
+			"imageEntityAlias",
+			"imageUrlAttribute",
+			"xPosKeyName",
+			"yPosKeyName",
+			"showLabel",
+			"label",
+			"useLabelFunction",
+			{
+				"key": "labelFunction",
+				"type": "javascript"
+			},
+			"showTooltip",
+			"autocloseTooltip",
+			{
+				"key": "tooltipPattern",
+				"type": "textarea"
+			},
+			"useTooltipFunction",
+			{
+				"key": "tooltipFunction",
+				"type": "javascript"
+			},
+			{
+				"key": "color",
+				"type": "color"
+			},
+			{
+				"key": "posFunction",
+				"type": "javascript"
+			},
+			"markerOffsetX",
+			"markerOffsetY",
+			"useColorFunction",
+			{
+				"key": "colorFunction",
+				"type": "javascript"
+			},
+			{
+				"key": "markerImage",
+				"type": "image"
+			},
+			"markerImageSize",
+			"useMarkerImageFunction",
+			{
+				"key": "markerImageFunction",
+				"type": "javascript"
+			},
+			{
+				"key": "markerImages",
+				"items": [
+					{
+						"key": "markerImages[]",
+						"type": "image"
+					}
+				]
+			}
+		]
+	};
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js
index 2f9c1bc..25e454f 100644
--- a/ui/src/app/widget/lib/openstreet-map.js
+++ b/ui/src/app/widget/lib/openstreet-map.js
@@ -19,234 +19,270 @@ import 'leaflet-providers';
 
 export default class TbOpenStreetMap {
 
-    constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, mapProvider) {
-
-        this.utils = utils;
-        this.defaultZoomLevel = defaultZoomLevel;
-        this.dontFitMapBounds = dontFitMapBounds;
-        this.minZoomLevel = minZoomLevel;
-        this.tooltips = [];
-
-        if (!mapProvider) {
-            mapProvider = "OpenStreetMap.Mapnik";
-        }
-
-        this.map = L.map($containerElement[0]).setView([0, 0], this.defaultZoomLevel || 8);
-
-        var tileLayer = L.tileLayer.provider(mapProvider);
-
-        tileLayer.addTo(this.map);
-
-        if (initCallback) {
-            setTimeout(initCallback, 0); //eslint-disable-line
-        }
-
-    }
-
-    inited() {
-        return angular.isDefined(this.map);
-    }
-
-    updateMarkerLabel(marker, settings) {
-        marker.unbindTooltip();
-        marker.bindTooltip('<div style="color: '+ settings.labelColor +';"><b>'+settings.labelText+'</b></div>',
-            { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset });
-    }
-
-    updateMarkerColor(marker, color) {
-        this.createDefaultMarkerIcon(marker, color, (iconInfo) => {
-            marker.setIcon(iconInfo.icon);
-        });
-    }
-
-    updateMarkerIcon(marker, settings) {
-        this.createMarkerIcon(marker, settings, (iconInfo) => {
-            marker.setIcon(iconInfo.icon);
-            if (settings.showLabel) {
-                marker.unbindTooltip();
-                marker.tooltipOffset = [0, -iconInfo.size[1] + 10];
-                marker.bindTooltip('<div style="color: '+ settings.labelColor +';"><b>'+settings.labelText+'</b></div>',
-                    { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset });
-            }
-        });
-    }
-
-    createMarkerIcon(marker, settings, onMarkerIconReady) {
-        var currentImage = settings.currentImage;
-        var opMap = this;
-        if (currentImage && currentImage.url) {
-            this.utils.loadImageAspect(currentImage.url).then(
-                (aspect) => {
-                    if (aspect) {
-                        var width;
-                        var height;
-                        if (aspect > 1) {
-                            width = currentImage.size;
-                            height = currentImage.size / aspect;
-                        } else {
-                            width = currentImage.size * aspect;
-                            height = currentImage.size;
-                        }
-                        var icon = L.icon({
-                            iconUrl: currentImage.url,
-                            iconSize: [width, height],
-                            iconAnchor: [width/2, height],
-                            popupAnchor: [0, -height]
-                        });
-                        var iconInfo = {
-                            size: [width, height],
-                            icon: icon
-                        };
-                        onMarkerIconReady(iconInfo);
-                    } else {
-                        opMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
-                    }
-                }
-            );
-        } else {
-            this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
-        }
-    }
-
-    createDefaultMarkerIcon(marker, color, onMarkerIconReady) {
-        var pinColor = color.substr(1);
-        var icon = L.icon({
-            iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor,
-            iconSize: [21, 34],
-            iconAnchor: [10, 34],
-            popupAnchor: [0, -34],
-            shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
-            shadowSize: [40, 37],
-            shadowAnchor: [12, 35]
-        });
-        var iconInfo = {
-            size: [21, 34],
-            icon: icon
-        };
-        onMarkerIconReady(iconInfo);
-    }
-
-    createMarker(location, dsIndex, settings, onClickListener, markerArgs) {
-        var marker = L.marker(location, {});
-        var opMap = this;
-        this.createMarkerIcon(marker, settings, (iconInfo) => {
-            marker.setIcon(iconInfo.icon);
-            if (settings.showLabel) {
-                marker.tooltipOffset = [0, -iconInfo.size[1] + 10];
-                marker.bindTooltip('<div style="color: '+ settings.labelColor +';"><b>'+settings.labelText+'</b></div>',
-                    { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset });
-            }
-            marker.addTo(opMap.map);
-        });
-
-        if (settings.displayTooltip) {
-            this.createTooltip(marker, dsIndex, settings, markerArgs);
-        }
-
-        if (onClickListener) {
-            marker.on('click', onClickListener);
-        }
-
-        return marker;
-    }
-
-    removeMarker(marker) {
-        this.map.removeLayer(marker);
-    }
-
-    createTooltip(marker, dsIndex, settings, markerArgs) {
-        var popup = L.popup();
-        popup.setContent('');
-        marker.bindPopup(popup, {autoClose: settings.autocloseTooltip, closeOnClick: false});
-        this.tooltips.push( {
-            markerArgs: markerArgs,
-            popup: popup,
-            locationSettings: settings,
-            dsIndex: dsIndex
-        });
-    }
-
-    updatePolylineColor(polyline, settings, color) {
-        var style = {
-            color: color,
-            opacity: settings.strokeOpacity,
-            weight: settings.strokeWeight
-        };
-        polyline.setStyle(style);
-    }
-
-    createPolyline(locations, settings) {
-        var polyline = L.polyline(locations,
-            {
-                color: settings.color,
-                opacity: settings.strokeOpacity,
-                weight: settings.strokeWeight
-            }
-        ).addTo(this.map);
-        return polyline;
-    }
-
-    removePolyline(polyline) {
-        this.map.removeLayer(polyline);
-    }
-
-    fitBounds(bounds) {
-        if (bounds.isValid()) {
-            if (this.dontFitMapBounds && this.defaultZoomLevel) {
-                this.map.setZoom(this.defaultZoomLevel, {animate: false});
-                this.map.panTo(bounds.getCenter(), {animate: false});
-            } else {
-                var tbMap = this;
-                this.map.once('zoomend', function() {
-                    if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) {
-                        tbMap.map.setZoom(tbMap.minZoomLevel, {animate: false});
-                    }
-                });
-                this.map.fitBounds(bounds, {padding: [50, 50], animate: false});
-            }
-        }
-    }
-
-    createLatLng(lat, lng) {
-        return L.latLng(lat, lng);
-    }
-
-    extendBoundsWithMarker(bounds, marker) {
-        bounds.extend(marker.getLatLng());
-    }
-
-    getMarkerPosition(marker) {
-        return marker.getLatLng();
-    }
-
-    setMarkerPosition(marker, latLng) {
-        marker.setLatLng(latLng);
-    }
-
-    getPolylineLatLngs(polyline) {
-        return polyline.getLatLngs();
-    }
-
-    setPolylineLatLngs(polyline, latLngs) {
-        polyline.setLatLngs(latLngs);
-    }
-
-    createBounds() {
-        return L.latLngBounds();
-    }
-
-    extendBounds(bounds, polyline) {
-        if (polyline && polyline.getLatLngs()) {
-            bounds.extend(polyline.getBounds());
-        }
-    }
-
-    invalidateSize() {
-        this.map.invalidateSize(true);
-    }
-
-    getTooltips() {
-        return this.tooltips;
-    }
+	constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, mapProvider) {
+
+		this.utils = utils;
+		this.defaultZoomLevel = defaultZoomLevel;
+		this.dontFitMapBounds = dontFitMapBounds;
+		this.minZoomLevel = minZoomLevel;
+		this.tooltips = [];
+
+		if (!mapProvider) {
+			mapProvider = "OpenStreetMap.Mapnik";
+		}
+
+		this.map = L.map($containerElement[0]).setView([0, 0], this.defaultZoomLevel || 8);
+
+		var tileLayer = L.tileLayer.provider(mapProvider);
+
+		tileLayer.addTo(this.map);
+
+		if (initCallback) {
+			setTimeout(initCallback, 0); //eslint-disable-line
+		}
+
+	}
+
+	inited() {
+		return angular.isDefined(this.map);
+	}
+
+	updateMarkerLabel(marker, settings) {
+		marker.unbindTooltip();
+		marker.bindTooltip('<div style="color: ' + settings.labelColor + ';"><b>' + settings.labelText + '</b></div>',
+			{className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset});
+	}
+
+	updateMarkerColor(marker, color) {
+		this.createDefaultMarkerIcon(marker, color, (iconInfo) => {
+			marker.setIcon(iconInfo.icon);
+		});
+	}
+
+	updateMarkerIcon(marker, settings) {
+		this.createMarkerIcon(marker, settings, (iconInfo) => {
+			marker.setIcon(iconInfo.icon);
+			if (settings.showLabel) {
+				marker.unbindTooltip();
+				marker.tooltipOffset = [0, -iconInfo.size[1] + 10];
+				marker.bindTooltip('<div style="color: ' + settings.labelColor + ';"><b>' + settings.labelText + '</b></div>',
+					{className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset});
+			}
+		});
+	}
+
+	createMarkerIcon(marker, settings, onMarkerIconReady) {
+		var currentImage = settings.currentImage;
+		var opMap = this;
+		if (currentImage && currentImage.url) {
+			this.utils.loadImageAspect(currentImage.url).then(
+				(aspect) => {
+					if (aspect) {
+						var width;
+						var height;
+						if (aspect > 1) {
+							width = currentImage.size;
+							height = currentImage.size / aspect;
+						} else {
+							width = currentImage.size * aspect;
+							height = currentImage.size;
+						}
+						var icon = L.icon({
+							iconUrl: currentImage.url,
+							iconSize: [width, height],
+							iconAnchor: [width / 2, height],
+							popupAnchor: [0, -height]
+						});
+						var iconInfo = {
+							size: [width, height],
+							icon: icon
+						};
+						onMarkerIconReady(iconInfo);
+					} else {
+						opMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
+					}
+				}
+			);
+		} else {
+			this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
+		}
+	}
+
+	createDefaultMarkerIcon(marker, color, onMarkerIconReady) {
+		var pinColor = color.substr(1);
+		var icon = L.icon({
+			iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor,
+			iconSize: [21, 34],
+			iconAnchor: [10, 34],
+			popupAnchor: [0, -34],
+			shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
+			shadowSize: [40, 37],
+			shadowAnchor: [12, 35]
+		});
+		var iconInfo = {
+			size: [21, 34],
+			icon: icon
+		};
+		onMarkerIconReady(iconInfo);
+	}
+
+	createMarker(location, dsIndex, settings, onClickListener, markerArgs) {
+		var marker = L.marker(location, {});
+		var opMap = this;
+		this.createMarkerIcon(marker, settings, (iconInfo) => {
+			marker.setIcon(iconInfo.icon);
+			if (settings.showLabel) {
+				marker.tooltipOffset = [0, -iconInfo.size[1] + 10];
+				marker.bindTooltip('<div style="color: ' + settings.labelColor + ';"><b>' + settings.labelText + '</b></div>',
+					{className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset});
+			}
+			marker.addTo(opMap.map);
+		});
+
+		if (settings.displayTooltip) {
+			this.createTooltip(marker, dsIndex, settings, markerArgs);
+		}
+
+		if (onClickListener) {
+			marker.on('click', onClickListener);
+		}
+
+		return marker;
+	}
+
+	removeMarker(marker) {
+		this.map.removeLayer(marker);
+	}
+
+	createTooltip(marker, dsIndex, settings, markerArgs) {
+		var popup = L.popup();
+		popup.setContent('');
+		marker.bindPopup(popup, {autoClose: settings.autocloseTooltip, closeOnClick: false});
+		this.tooltips.push({
+			markerArgs: markerArgs,
+			popup: popup,
+			locationSettings: settings,
+			dsIndex: dsIndex
+		});
+	}
+
+	updatePolylineColor(polyline, settings, color) {
+		var style = {
+			color: color,
+			opacity: settings.strokeOpacity,
+			weight: settings.strokeWeight
+		};
+		polyline.setStyle(style);
+	}
+
+	createPolyline(locations, settings) {
+		var polyline = L.polyline(locations,
+			{
+				color: settings.color,
+				opacity: settings.strokeOpacity,
+				weight: settings.strokeWeight
+			}
+		).addTo(this.map);
+		return polyline;
+	}
+
+	removePolyline(polyline) {
+		this.map.removeLayer(polyline);
+	}
+
+	createPolygon(latLangs, settings) {
+		let polygon = L.polygon(latLangs, {
+			fill: true,
+			fillColor: settings.polygonColor,
+			color: settings.polygonStrokeColor,
+			weight: settings.polygonStrokeWeight,
+			fillOpacity: settings.polygonOpacity,
+			opacity: settings.polygonStrokeOpacity
+		}).addTo(this.map);
+		return polygon;
+	}
+
+	removePolygon(polygon) {
+		this.map.removeLayer(polygon);
+	}
+
+	updatePolygonColor(polygon, settings, color) {
+		let style = {
+			fill: true,
+			fillColor: color,
+			color: color,
+			weight: settings.polygonStrokeWeight,
+			fillOpacity: settings.polygonOpacity,
+			opacity: settings.polygonStrokeOpacity
+		};
+		polygon.setStyle(style);
+	}
+
+	getPolygonLatLngs(polygon) {
+		return polygon.getLatLngs();
+	}
+
+	setPolygonLatLngs(polygon, latLngs) {
+		polygon.setLatLngs(latLngs);
+	}
+
+	fitBounds(bounds) {
+		if (bounds.isValid()) {
+			if (this.dontFitMapBounds && this.defaultZoomLevel) {
+				this.map.setZoom(this.defaultZoomLevel, {animate: false});
+				this.map.panTo(bounds.getCenter(), {animate: false});
+			} else {
+				var tbMap = this;
+				this.map.once('zoomend', function () {
+					if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) {
+						tbMap.map.setZoom(tbMap.minZoomLevel, {animate: false});
+					}
+				});
+				this.map.fitBounds(bounds, {padding: [50, 50], animate: false});
+			}
+		}
+	}
+
+	createLatLng(lat, lng) {
+		return L.latLng(lat, lng);
+	}
+
+	extendBoundsWithMarker(bounds, marker) {
+		bounds.extend(marker.getLatLng());
+	}
+
+	getMarkerPosition(marker) {
+		return marker.getLatLng();
+	}
+
+	setMarkerPosition(marker, latLng) {
+		marker.setLatLng(latLng);
+	}
+
+	getPolylineLatLngs(polyline) {
+		return polyline.getLatLngs();
+	}
+
+	setPolylineLatLngs(polyline, latLngs) {
+		polyline.setLatLngs(latLngs);
+	}
+
+	createBounds() {
+		return L.latLngBounds();
+	}
+
+	extendBounds(bounds, polyline) {
+		if (polyline && polyline.getLatLngs()) {
+			bounds.extend(polyline.getBounds());
+		}
+	}
+
+	invalidateSize() {
+		this.map.invalidateSize(true);
+	}
+
+	getTooltips() {
+		return this.tooltips;
+	}
 
 }
diff --git a/ui/src/app/widget/lib/tencent-map.js b/ui/src/app/widget/lib/tencent-map.js
index 94fc43a..17a6121 100644
--- a/ui/src/app/widget/lib/tencent-map.js
+++ b/ui/src/app/widget/lib/tencent-map.js
@@ -14,377 +14,425 @@
  * limitations under the License.
  */
 var tmGlobals = {
-    loadingTmId: null,
-    tmApiKeys: {}
+	loadingTmId: null,
+	tmApiKeys: {}
 }
 
 export default class TbTencentMap {
-    constructor($containerElement,utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, tmApiKey, tmDefaultMapType) {
-        var tbMap = this;
-        this.utils = utils;
-        this.defaultZoomLevel = defaultZoomLevel;
-        this.dontFitMapBounds = dontFitMapBounds;
-        this.minZoomLevel = minZoomLevel;
-        this.tooltips = [];
-        this.defaultMapType = tmDefaultMapType;
-
-        function clearGlobalId() {
-            if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
-                tmGlobals.loadingTmId = null;
-            }
-        }
-
-        function displayError(message) {
-            $containerElement.html( // eslint-disable-line angular/angularelement
-                "<div class='error'>"+ message + "</div>"
-            );
-        }
-
-        function initTencentMap() {
-            tbMap.map = new qq.maps.Map($containerElement[0], { // eslint-disable-line no-undef
-                scrollwheel: true,
-                mapTypeId: getTencentMapTypeId(tbMap.defaultMapType),
-                zoom: tbMap.defaultZoomLevel || 8
-            });
-
-            if (initCallback) {
-                initCallback();
-            }
-        }
-
-        /* eslint-disable no-undef */
-
-        function getTencentMapTypeId(mapType) {
-            var mapTypeId =qq.maps.MapTypeId.ROADMAP;
-            if (mapType) {
-                if (mapType === 'hybrid') {
-                   mapTypeId = qq.maps.MapTypeId.HYBRID;
-                } else if (mapType === 'satellite') {
-                   mapTypeId = qq.maps.MapTypeId.SATELLITE;
-                } else if (mapType === 'terrain') {
-                   mapTypeId = qq.maps.MapTypeId.ROADMAP;
-                }
-            }
-            return mapTypeId;
-        }
-
-        /* eslint-enable no-undef */
-
-        this.mapId = '' + Math.random().toString(36).substr(2, 9);
-        this.apiKey = tmApiKey || '84d6d83e0e51e481e50454ccbe8986b';
-
-        window.tm_authFailure = function() { // eslint-disable-line no-undef, angular/window-service
-            if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
-                tmGlobals.loadingTmId = null;
-                tmGlobals.tmApiKeys[tbMap.apiKey].error = 'Unable to authentificate for tencent Map API.</br>Please check your API key.';
-                displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
-            }
-        };
-
-        this.initMapFunctionName = 'initTencentMap_' + this.mapId;
-
-        window[this.initMapFunctionName] = function() { // eslint-disable-line no-undef, angular/window-service
-            tmGlobals.tmApiKeys[tbMap.apiKey].loaded = true;
-            initTencentMap();
-            for (var p = 0; p < tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits.length; p++) {
-                var pendingInit = tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits[p];
-                pendingInit();
-            }
-            tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits = [];
-        };
-        if (this.apiKey && this.apiKey.length > 0) {
-            if (tmGlobals.tmApiKeys[this.apiKey]) {
-                if (tmGlobals.tmApiKeys[this.apiKey].error) {
-                    displayError(tmGlobals.tmApiKeys[this.apiKey].error);
-                } else if (tmGlobals.tmApiKeys[this.apiKey].loaded) {
-                    initTencentMap();
-                } else {
-                    tmGlobals.tmApiKeys[this.apiKey].pendingInits.push(initTencentMap);
-                }
-            } else {
-                tmGlobals.tmApiKeys[this.apiKey] = {
-                    loaded: false,
-                    pendingInits: []
-                };
-                var tencentMapScriptRes = 'https://map.qq.com/api/js?v=2.exp&key='+this.apiKey+'&callback='+this.initMapFunctionName;
-
-                tmGlobals.loadingTmId = this.mapId;
-                lazyLoad.load({ type: 'js', path: tencentMapScriptRes }).then( // eslint-disable-line no-undef
-                    function success() {
-                        setTimeout(clearGlobalId, 2000); // eslint-disable-line no-undef, angular/timeout-service
-                    },
-                    function fail(e) {
-                        clearGlobalId();
-                        tmGlobals.tmApiKeys[tbMap.apiKey].error = 'tencent map api load failed!</br>'+e;
-                        displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
-                    }
-                );
-            }
-        } else {
-            displayError('No tencent Map Api Key provided!');
-        }
-    }
-
-    inited() {
-        return angular.isDefined(this.map);
-    }
-
-    createMarkerLabelStyle(settings) {
-        return {
-            width: "200px",
-            textAlign: "center",
-            color: settings.labelColor,
-            background: "none",
-            border: "none",
-            fontSize: "12px",
-            fontFamily: "\"Helvetica Neue\", Arial, Helvetica, sans-serif",
-            fontWeight: "bold"
-        };
-    }
-
-    /* eslint-disable no-undef,no-unused-vars*/
-    updateMarkerLabel(marker, settings) {
-        if (marker.label) {
-            marker.label.setContent(settings.labelText);
-            marker.label.setStyle(this.createMarkerLabelStyle(settings));
-        }
-    }
-    /* eslint-enable no-undef,no-unused-vars */
-
-    /* eslint-disable no-undef,no-unused-vars */
-    updateMarkerColor(marker, color) {
-        this.createDefaultMarkerIcon(marker, color, (iconInfo) => {
-            marker.setIcon(iconInfo.icon);
-        });
-    }
-    /* eslint-enable no-undef,,no-unused-vars */
-
-    /* eslint-disable no-undef */
-    updateMarkerIcon(marker, settings) {
-        this.createMarkerIcon(marker, settings, (iconInfo) => {
-            marker.setIcon(iconInfo.icon);
-            if (marker.label) {
-                marker.label.setOffset(new qq.maps.Size(-100, -iconInfo.size[1]-20));
-            }
-        });
-    }
-    /* eslint-disable no-undef */
-
-    /* eslint-disable no-undef */
-    createMarkerIcon(marker, settings, onMarkerIconReady) {
-        var currentImage = settings.currentImage;
-        var tMap = this;
-        if (currentImage && currentImage.url) {
-            this.utils.loadImageAspect(currentImage.url).then(
-                (aspect) => {
-                    if (aspect) {
-                        var width;
-                        var height;
-                        if (aspect > 1) {
-                            width = currentImage.size;
-                            height = currentImage.size / aspect;
-                        } else {
-                            width = currentImage.size * aspect;
-                            height = currentImage.size;
-                        }
-                        var icon = new qq.maps.MarkerImage(currentImage.url,
-                            new qq.maps.Size(width, height),
-                            new qq.maps.Point(0,0),
-                            new qq.maps.Point(width/2, height),
-                            new qq.maps.Size(width, height));
-                        var iconInfo = {
-                            size: [width, height],
-                            icon: icon
-                        };
-                        onMarkerIconReady(iconInfo);
-                    } else {
-                        tMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
-                    }
-                }
-            );
-        } else {
-            this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
-        }
-    }
-    /* eslint-enable no-undef */
-
-    /* eslint-disable no-undef */
-    createDefaultMarkerIcon(marker, color, onMarkerIconReady) {
-        var pinColor = color.substr(1);
-        var icon = new qq.maps.MarkerImage("https://chart.apis.google.com/chart?chst=d_map_pin_letter_withshadow&chld=%E2%80%A2|" + pinColor,
-            new qq.maps.Size(40, 37),
-            new qq.maps.Point(0,0),
-            new qq.maps.Point(10, 37));
-        var iconInfo = {
-            size: [40, 37],
-            icon: icon
-        };
-        onMarkerIconReady(iconInfo);
-    }
-    /* eslint-enable no-undef */
-
-    /* eslint-disable no-undef */
-    createMarker(location, dsIndex, settings, onClickListener, markerArgs) {
-        var marker = new qq.maps.Marker({
-            position: location
-        });
-        var tMap = this;
-        this.createMarkerIcon(marker, settings, (iconInfo) => {
-            marker.setIcon(iconInfo.icon);
-            marker.setMap(tMap.map);
-            if (settings.showLabel) {
-                marker.label = new qq.maps.Label({
-                    clickable: false,
-                    content: settings.labelText,
-                    offset: new qq.maps.Size(-100, -iconInfo.size[1]-20),
-                    style: tMap.createMarkerLabelStyle(settings),
-                    visible: true,
-                    position: location,
-                    map: tMap.map,
-                    zIndex: 1000
-                });
-            }
-        });
-
-        if (settings.displayTooltip) {
-            this.createTooltip(marker, dsIndex, settings, markerArgs);
-        }
-
-        if (onClickListener) {
-            qq.maps.event.addListener(marker, 'click', onClickListener);
-        }
-
-        return marker;
-    }
-
-    /* eslint-disable no-undef */
-    removeMarker(marker) {
-        marker.setMap(null);
-        if (marker.label) {
-            marker.label.setMap(null);
-        }
-    }
-
-    /* eslint-enable no-undef */
-
-    /* eslint-disable no-undef */
-    createTooltip(marker, dsIndex, settings, markerArgs) {
-        var popup = new qq.maps.InfoWindow({
-            map :this.map
-        });
-        var map = this;
-        qq.maps.event.addListener(marker, 'click', function() {
-            if (settings.autocloseTooltip) {
-                map.tooltips.forEach((tooltip) => {
-                    tooltip.popup.close();
-                });
-            }
-            popup.open();
-            popup.setPosition(marker);
-        });
-        this.tooltips.push( {
-            markerArgs: markerArgs,
-            popup: popup,
-            locationSettings: settings,
-            dsIndex: dsIndex
-        });
-    }
-    /* eslint-enable no-undef */
-
-    /* eslint-disable no-undef */
-    updatePolylineColor(polyline, settings, color) {
-        var options = {
-            path: polyline.getPath(),
-            strokeColor: color,
-            strokeOpacity: settings.strokeOpacity,
-            strokeWeight: settings.strokeWeight,
-            map: this.map
-        };
-        polyline.setOptions(options);
-    }
-    /* eslint-enable no-undef */
-
-    /* eslint-disable no-undef */
-    createPolyline(locations, settings) {
-        var polyline = new qq.maps.Polyline({
-            path: locations,
-            strokeColor: settings.color,
-            strokeOpacity: settings.strokeOpacity,
-            strokeWeight: settings.strokeWeight,
-            map: this.map
-        });
-
-        return polyline;
-    }
-    /* eslint-enable no-undef */
-
-    removePolyline(polyline) {
-        polyline.setMap(null);
-    }
-
-    /* eslint-disable no-undef ,no-unused-vars*/
-    fitBounds(bounds) {
-        if (this.dontFitMapBounds && this.defaultZoomLevel) {
-            this.map.setZoom(this.defaultZoomLevel);
-            this.map.setCenter(bounds.getCenter());
-        } else {
-            var tbMap = this;
-            qq.maps.event.addListenerOnce(this.map, 'bounds_changed', function() { // eslint-disable-line no-undef
-                if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) {
-                    tbMap.map.setZoom(tbMap.minZoomLevel);
-                }
-            });
-            this.map.fitBounds(bounds);
-        }
-    }
-    /* eslint-enable no-undef,no-unused-vars */
-
-    createLatLng(lat, lng) {
-        return new qq.maps.LatLng(lat, lng); // eslint-disable-line no-undef
-    }
-
-    extendBoundsWithMarker(bounds, marker) {
-        bounds.extend(marker.getPosition());
-    }
-
-    getMarkerPosition(marker) {
-        return marker.getPosition();
-    }
-
-    setMarkerPosition(marker, latLng) {
-        marker.setPosition(latLng);
-        if (marker.label) {
-            marker.label.setPosition(latLng);
-        }
-    }
-
-    getPolylineLatLngs(polyline) {
-        return polyline.getPath().getArray();
-    }
-
-    setPolylineLatLngs(polyline, latLngs) {
-        polyline.setPath(latLngs);
-    }
-
-    createBounds() {
-        return new qq.maps.LatLngBounds(); // eslint-disable-line no-undef
-    }
-
-    extendBounds(bounds, polyline) {
-        if (polyline && polyline.getPath()) {
-            var locations = polyline.getPath();
-            for (var i = 0; i < locations.getLength(); i++) {
-                bounds.extend(locations.getAt(i));
-            }
-        }
-    }
-
-    invalidateSize() {
-        qq.maps.event.trigger(this.map, "resize"); // eslint-disable-line no-undef
-    }
-
-    getTooltips() {
-        return this.tooltips;
-    }
+	constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, tmApiKey, tmDefaultMapType) {
+		var tbMap = this;
+		this.utils = utils;
+		this.defaultZoomLevel = defaultZoomLevel;
+		this.dontFitMapBounds = dontFitMapBounds;
+		this.minZoomLevel = minZoomLevel;
+		this.tooltips = [];
+		this.defaultMapType = tmDefaultMapType;
+
+		function clearGlobalId() {
+			if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
+				tmGlobals.loadingTmId = null;
+			}
+		}
+
+		function displayError(message) {
+			$containerElement.html( // eslint-disable-line angular/angularelement
+				"<div class='error'>" + message + "</div>"
+			);
+		}
+
+		function initTencentMap() {
+			tbMap.map = new qq.maps.Map($containerElement[0], { // eslint-disable-line no-undef
+				scrollwheel: true,
+				mapTypeId: getTencentMapTypeId(tbMap.defaultMapType),
+				zoom: tbMap.defaultZoomLevel || 8
+			});
+
+			if (initCallback) {
+				initCallback();
+			}
+		}
+
+		/* eslint-disable no-undef */
+
+		function getTencentMapTypeId(mapType) {
+			var mapTypeId = qq.maps.MapTypeId.ROADMAP;
+			if (mapType) {
+				if (mapType === 'hybrid') {
+					mapTypeId = qq.maps.MapTypeId.HYBRID;
+				} else if (mapType === 'satellite') {
+					mapTypeId = qq.maps.MapTypeId.SATELLITE;
+				} else if (mapType === 'terrain') {
+					mapTypeId = qq.maps.MapTypeId.ROADMAP;
+				}
+			}
+			return mapTypeId;
+		}
+
+		/* eslint-enable no-undef */
+
+		this.mapId = '' + Math.random().toString(36).substr(2, 9);
+		this.apiKey = tmApiKey || '84d6d83e0e51e481e50454ccbe8986b';
+
+		window.tm_authFailure = function () { // eslint-disable-line no-undef, angular/window-service
+			if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
+				tmGlobals.loadingTmId = null;
+				tmGlobals.tmApiKeys[tbMap.apiKey].error = 'Unable to authentificate for tencent Map API.</br>Please check your API key.';
+				displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
+			}
+		};
+
+		this.initMapFunctionName = 'initTencentMap_' + this.mapId;
+
+		window[this.initMapFunctionName] = function () { // eslint-disable-line no-undef, angular/window-service
+			tmGlobals.tmApiKeys[tbMap.apiKey].loaded = true;
+			initTencentMap();
+			for (var p = 0; p < tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits.length; p++) {
+				var pendingInit = tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits[p];
+				pendingInit();
+			}
+			tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits = [];
+		};
+		if (this.apiKey && this.apiKey.length > 0) {
+			if (tmGlobals.tmApiKeys[this.apiKey]) {
+				if (tmGlobals.tmApiKeys[this.apiKey].error) {
+					displayError(tmGlobals.tmApiKeys[this.apiKey].error);
+				} else if (tmGlobals.tmApiKeys[this.apiKey].loaded) {
+					initTencentMap();
+				} else {
+					tmGlobals.tmApiKeys[this.apiKey].pendingInits.push(initTencentMap);
+				}
+			} else {
+				tmGlobals.tmApiKeys[this.apiKey] = {
+					loaded: false,
+					pendingInits: []
+				};
+				var tencentMapScriptRes = 'https://map.qq.com/api/js?v=2.exp&key=' + this.apiKey + '&callback=' + this.initMapFunctionName;
+
+				tmGlobals.loadingTmId = this.mapId;
+				lazyLoad.load({type: 'js', path: tencentMapScriptRes}).then( // eslint-disable-line no-undef
+					function success() {
+						setTimeout(clearGlobalId, 2000); // eslint-disable-line no-undef, angular/timeout-service
+					},
+					function fail(e) {
+						clearGlobalId();
+						tmGlobals.tmApiKeys[tbMap.apiKey].error = 'tencent map api load failed!</br>' + e;
+						displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
+					}
+				);
+			}
+		} else {
+			displayError('No tencent Map Api Key provided!');
+		}
+	}
+
+	inited() {
+		return angular.isDefined(this.map);
+	}
+
+	createMarkerLabelStyle(settings) {
+		return {
+			width: "200px",
+			textAlign: "center",
+			color: settings.labelColor,
+			background: "none",
+			border: "none",
+			fontSize: "12px",
+			fontFamily: "\"Helvetica Neue\", Arial, Helvetica, sans-serif",
+			fontWeight: "bold"
+		};
+	}
+
+	/* eslint-disable no-undef,no-unused-vars*/
+	updateMarkerLabel(marker, settings) {
+		if (marker.label) {
+			marker.label.setContent(settings.labelText);
+			marker.label.setStyle(this.createMarkerLabelStyle(settings));
+		}
+	}
+
+	/* eslint-enable no-undef,no-unused-vars */
+
+	/* eslint-disable no-undef,no-unused-vars */
+	updateMarkerColor(marker, color) {
+		this.createDefaultMarkerIcon(marker, color, (iconInfo) => {
+			marker.setIcon(iconInfo.icon);
+		});
+	}
+
+	/* eslint-enable no-undef,,no-unused-vars */
+
+	/* eslint-disable no-undef */
+	updateMarkerIcon(marker, settings) {
+		this.createMarkerIcon(marker, settings, (iconInfo) => {
+			marker.setIcon(iconInfo.icon);
+			if (marker.label) {
+				marker.label.setOffset(new qq.maps.Size(-100, -iconInfo.size[1] - 20));
+			}
+		});
+	}
+
+	/* eslint-disable no-undef */
+
+	/* eslint-disable no-undef */
+	createMarkerIcon(marker, settings, onMarkerIconReady) {
+		var currentImage = settings.currentImage;
+		var tMap = this;
+		if (currentImage && currentImage.url) {
+			this.utils.loadImageAspect(currentImage.url).then(
+				(aspect) => {
+					if (aspect) {
+						var width;
+						var height;
+						if (aspect > 1) {
+							width = currentImage.size;
+							height = currentImage.size / aspect;
+						} else {
+							width = currentImage.size * aspect;
+							height = currentImage.size;
+						}
+						var icon = new qq.maps.MarkerImage(currentImage.url,
+							new qq.maps.Size(width, height),
+							new qq.maps.Point(0, 0),
+							new qq.maps.Point(width / 2, height),
+							new qq.maps.Size(width, height));
+						var iconInfo = {
+							size: [width, height],
+							icon: icon
+						};
+						onMarkerIconReady(iconInfo);
+					} else {
+						tMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
+					}
+				}
+			);
+		} else {
+			this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
+		}
+	}
+
+	/* eslint-enable no-undef */
+
+	/* eslint-disable no-undef */
+	createDefaultMarkerIcon(marker, color, onMarkerIconReady) {
+		var pinColor = color.substr(1);
+		var icon = new qq.maps.MarkerImage("https://chart.apis.google.com/chart?chst=d_map_pin_letter_withshadow&chld=%E2%80%A2|" + pinColor,
+			new qq.maps.Size(40, 37),
+			new qq.maps.Point(0, 0),
+			new qq.maps.Point(10, 37));
+		var iconInfo = {
+			size: [40, 37],
+			icon: icon
+		};
+		onMarkerIconReady(iconInfo);
+	}
+
+	/* eslint-enable no-undef */
+
+	/* eslint-disable no-undef */
+	createMarker(location, dsIndex, settings, onClickListener, markerArgs) {
+		var marker = new qq.maps.Marker({
+			position: location
+		});
+		var tMap = this;
+		this.createMarkerIcon(marker, settings, (iconInfo) => {
+			marker.setIcon(iconInfo.icon);
+			marker.setMap(tMap.map);
+			if (settings.showLabel) {
+				marker.label = new qq.maps.Label({
+					clickable: false,
+					content: settings.labelText,
+					offset: new qq.maps.Size(-100, -iconInfo.size[1] - 20),
+					style: tMap.createMarkerLabelStyle(settings),
+					visible: true,
+					position: location,
+					map: tMap.map,
+					zIndex: 1000
+				});
+			}
+		});
+
+		if (settings.displayTooltip) {
+			this.createTooltip(marker, dsIndex, settings, markerArgs);
+		}
+
+		if (onClickListener) {
+			qq.maps.event.addListener(marker, 'click', onClickListener);
+		}
+
+		return marker;
+	}
+
+	/* eslint-disable no-undef */
+	removeMarker(marker) {
+		marker.setMap(null);
+		if (marker.label) {
+			marker.label.setMap(null);
+		}
+	}
+
+	/* eslint-enable no-undef */
+
+	/* eslint-disable no-undef */
+	createTooltip(marker, dsIndex, settings, markerArgs) {
+		var popup = new qq.maps.InfoWindow({
+			map: this.map
+		});
+		var map = this;
+		qq.maps.event.addListener(marker, 'click', function () {
+			if (settings.autocloseTooltip) {
+				map.tooltips.forEach((tooltip) => {
+					tooltip.popup.close();
+				});
+			}
+			popup.open();
+			popup.setPosition(marker);
+		});
+		this.tooltips.push({
+			markerArgs: markerArgs,
+			popup: popup,
+			locationSettings: settings,
+			dsIndex: dsIndex
+		});
+	}
+
+	/* eslint-enable no-undef */
+
+	/* eslint-disable no-undef */
+	updatePolylineColor(polyline, settings, color) {
+		var options = {
+			path: polyline.getPath(),
+			strokeColor: color,
+			strokeOpacity: settings.strokeOpacity,
+			strokeWeight: settings.strokeWeight,
+			map: this.map
+		};
+		polyline.setOptions(options);
+	}
+
+	/* eslint-enable no-undef */
+
+	/* eslint-disable no-undef */
+	createPolyline(locations, settings) {
+		var polyline = new qq.maps.Polyline({
+			path: locations,
+			strokeColor: settings.color,
+			strokeOpacity: settings.strokeOpacity,
+			strokeWeight: settings.strokeWeight,
+			map: this.map
+		});
+
+		return polyline;
+	}
+
+	/* eslint-enable no-undef */
+
+	removePolyline(polyline) {
+		polyline.setMap(null);
+	}
+
+	/* eslint-disable no-undef */
+	createPolygon(latLangs, settings) {
+		let polygon = new qq.maps.Polygon({
+			map: this.map,
+			path: latLangs,
+			strokeColor: settings.polygonStrokeColor,
+			fillColor: settings.polygonColor,
+			strokeWeight: settings.polygonStrokeWeight
+		});
+		return polygon;
+	}
+	/* eslint-disable no-undef */
+
+	removePolygon (polygon) {
+		polygon.setMap(null);
+	}
+
+	/* eslint-disable no-undef,no-unused-vars */
+	updatePolygonColor (polygon, settings, color) {
+		let options = {
+			path: polygon.getPath(),
+			map: this.map,
+			strokeColor: color,
+			fillColor: color,
+			strokeWeight: settings.polygonStrokeWeight
+		}
+
+	}
+	/* eslint-disable no-undef ,no-unused-vars*/
+
+
+	getPolygonLatLngs(polygon) {
+		return polygon.getPath().getArray();
+	}
+
+	setPolygonLatLngs(polygon, latLngs) {
+		polygon.setPath(latLngs);
+	}
+
+	/* eslint-disable no-undef ,no-unused-vars*/
+	fitBounds(bounds) {
+		if (this.dontFitMapBounds && this.defaultZoomLevel) {
+			this.map.setZoom(this.defaultZoomLevel);
+			this.map.setCenter(bounds.getCenter());
+		} else {
+			var tbMap = this;
+			qq.maps.event.addListenerOnce(this.map, 'bounds_changed', function () { // eslint-disable-line no-undef
+				if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) {
+					tbMap.map.setZoom(tbMap.minZoomLevel);
+				}
+			});
+			this.map.fitBounds(bounds);
+		}
+	}
+
+	/* eslint-enable no-undef,no-unused-vars */
+
+	createLatLng(lat, lng) {
+		return new qq.maps.LatLng(lat, lng); // eslint-disable-line no-undef
+	}
+
+	extendBoundsWithMarker(bounds, marker) {
+		bounds.extend(marker.getPosition());
+	}
+
+	getMarkerPosition(marker) {
+		return marker.getPosition();
+	}
+
+	setMarkerPosition(marker, latLng) {
+		marker.setPosition(latLng);
+		if (marker.label) {
+			marker.label.setPosition(latLng);
+		}
+	}
+
+	getPolylineLatLngs(polyline) {
+		return polyline.getPath().getArray();
+	}
+
+	setPolylineLatLngs(polyline, latLngs) {
+		polyline.setPath(latLngs);
+	}
+
+	createBounds() {
+		return new qq.maps.LatLngBounds(); // eslint-disable-line no-undef
+	}
+
+	extendBounds(bounds, polyline) {
+		if (polyline && polyline.getPath()) {
+			var locations = polyline.getPath();
+			for (var i = 0; i < locations.getLength(); i++) {
+				bounds.extend(locations.getAt(i));
+			}
+		}
+	}
+
+	invalidateSize() {
+		qq.maps.event.trigger(this.map, "resize"); // eslint-disable-line no-undef
+	}
+
+	getTooltips() {
+		return this.tooltips;
+	}
 
 }