thingsboard-memoizeit

Merge with master

7/19/2018 9:49:07 AM

Changes

common/pom.xml 2(+1 -1)

dao/pom.xml 2(+1 -1)

pom.xml 2(+1 -1)

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

ui/package.json 22(+12 -10)

ui/pom.xml 2(+1 -1)

Details

diff --git a/application/pom.xml b/application/pom.xml
index 7f11d0d..0e65c76 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <artifactId>application</artifactId>
diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf
index a569549..2a80158 100644
--- a/application/src/main/conf/thingsboard.conf
+++ b/application/src/main/conf/thingsboard.conf
@@ -15,7 +15,7 @@
 #
 
 export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@ -Dinstall.data_dir=@pkg.installFolder@/data"
-export JAVA_OPTS="$JAVA_OPTS -Xloggc:@pkg.logFolder@/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
+export JAVA_OPTS="$JAVA_OPTS -Xloggc:@pkg.logFolder@/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
 export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
 export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
 export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index d453e59..8e7a9ae 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -111,6 +111,7 @@ public class AppActor extends RuleChainManagerActor {
             case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG:
             case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG:
             case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
+            case REMOTE_TO_RULE_CHAIN_TELL_NEXT_MSG:
                 onToDeviceActorMsg((TenantAwareMsg) msg);
                 break;
             case ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG:
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
index 3f3f70b..9e38c17 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -29,11 +29,7 @@ import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.service.cluster.discovery.ServerInstance;
 
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Queue;
-import java.util.UUID;
+import java.util.*;
 
 /**
  * @author Andrew Shvayka
@@ -88,7 +84,17 @@ public class RpcManagerActor extends ContextAwareActor {
 
     private void onMsg(RpcBroadcastMsg msg) {
         log.debug("Forwarding msg to session actors {}", msg);
-        sessionActors.keySet().forEach(address -> onMsg(msg.getMsg()));
+        sessionActors.keySet().forEach(address -> {
+            ClusterAPIProtos.ClusterMessage msgWithServerAddress = msg.getMsg()
+                    .toBuilder()
+                    .setServerAddress(ClusterAPIProtos.ServerAddress
+                            .newBuilder()
+                            .setHost(address.getHost())
+                            .setPort(address.getPort())
+                            .build())
+                    .build();
+            onMsg(msgWithServerAddress);
+        });
         pendingMsgs.values().forEach(queue -> queue.add(msg.getMsg()));
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java
new file mode 100644
index 0000000..fe0c2ef
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+
+import java.io.Serializable;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RemoteToRuleChainTellNextMsg extends RuleNodeToRuleChainTellNextMsg implements TenantAwareMsg, RuleChainAwareMsg, Serializable {
+
+    private static final long serialVersionUID = 2459605482321657447L;
+    private final TenantId tenantId;
+    private final RuleChainId ruleChainId;
+
+    public RemoteToRuleChainTellNextMsg(RuleNodeToRuleChainTellNextMsg original, TenantId tenantId, RuleChainId ruleChainId) {
+        super(original.getOriginator(), original.getRelationTypes(), original.getMsg());
+        this.tenantId = tenantId;
+        this.ruleChainId = ruleChainId;
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.REMOTE_TO_RULE_CHAIN_TELL_NEXT_MSG;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
index 3ba646a..c1a55fb 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
@@ -49,6 +49,7 @@ public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMe
                 processor.onDeviceActorToRuleEngineMsg((DeviceActorToRuleEngineMsg) msg);
                 break;
             case RULE_TO_RULE_CHAIN_TELL_NEXT_MSG:
+            case REMOTE_TO_RULE_CHAIN_TELL_NEXT_MSG:
                 processor.onTellNext((RuleNodeToRuleChainTellNextMsg) msg);
                 break;
             case RULE_CHAIN_TO_RULE_CHAIN_MSG:
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
index 7d560db..fe02335 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
@@ -20,6 +20,9 @@ import akka.actor.ActorRef;
 import akka.actor.Props;
 import akka.event.LoggingAdapter;
 import com.datastax.driver.core.utils.UUIDs;
+
+import java.util.Optional;
+
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.device.DeviceActorToRuleEngineMsg;
 import org.thingsboard.server.actors.device.RuleEngineQueuePutAckMsg;
@@ -37,6 +40,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
 import org.thingsboard.server.common.data.rule.RuleNode;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.dao.rule.RuleChainService;
@@ -217,16 +221,36 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
 
     void onTellNext(RuleNodeToRuleChainTellNextMsg envelope) {
         checkActive();
-        RuleNodeId originator = envelope.getOriginator();
-        List<RuleNodeRelation> relations = nodeRoutes.get(originator).stream()
-                .filter(r -> contains(envelope.getRelationTypes(), r.getType()))
-                .collect(Collectors.toList());
+        TbMsg msg = envelope.getMsg();
+        EntityId originatorEntityId = msg.getOriginator();
+        Optional<ServerAddress> address = systemContext.getRoutingService().resolveById(originatorEntityId);
+
+        if (address.isPresent()) {
+            onRemoteTellNext(address.get(), envelope);
+        } else {
+            onLocalTellNext(envelope);
+        }
+    }
+
+    private void onRemoteTellNext(ServerAddress serverAddress, RuleNodeToRuleChainTellNextMsg envelope) {
+        TbMsg msg = envelope.getMsg();
+        logger.debug("Forwarding [{}] msg to remote server [{}] due to changed originator id: [{}]", msg.getId(), serverAddress, msg.getOriginator());
+        envelope = new RemoteToRuleChainTellNextMsg(envelope, tenantId, entityId);
+        systemContext.getRpcService().tell(systemContext.getEncodingService().convertToProtoDataMessage(serverAddress, envelope));
+    }
 
+    private void onLocalTellNext(RuleNodeToRuleChainTellNextMsg envelope) {
         TbMsg msg = envelope.getMsg();
+        RuleNodeId originatorNodeId = envelope.getOriginator();
+        List<RuleNodeRelation> relations = nodeRoutes.get(originatorNodeId).stream()
+                .filter(r -> contains(envelope.getRelationTypes(), r.getType()))
+                .collect(Collectors.toList());
         int relationsCount = relations.size();
         EntityId ackId = msg.getRuleNodeId() != null ? msg.getRuleNodeId() : msg.getRuleChainId();
         if (relationsCount == 0) {
-            queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+            if (ackId != null) {
+                queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+            }
         } else if (relationsCount == 1) {
             for (RuleNodeRelation relation : relations) {
                 pushToTarget(msg, relation.getOut(), relation.getType());
@@ -244,7 +268,9 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
                 }
             }
             //TODO: Ideally this should happen in async way when all targets confirm that the copied messages are successfully written to corresponding target queues.
-            queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+            if (ackId != null) {
+                queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+            }
         }
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
index 8b13747..ae4ae45 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
@@ -20,12 +20,13 @@ import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.msg.MsgType;
 import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
 
 /**
  * Created by ashvayka on 19.03.18.
  */
 @Data
-public final class RuleChainToRuleChainMsg implements TbActorMsg {
+public final class RuleChainToRuleChainMsg implements TbActorMsg, RuleChainAwareMsg {
 
     private final RuleChainId target;
     private final RuleChainId source;
@@ -34,6 +35,11 @@ public final class RuleChainToRuleChainMsg implements TbActorMsg {
     private final boolean enqueue;
 
     @Override
+    public RuleChainId getRuleChainId() {
+        return target;
+    }
+
+    @Override
     public MsgType getMsgType() {
         return MsgType.RULE_CHAIN_TO_RULE_CHAIN_MSG;
     }
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
index c0a475c..9414892 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
@@ -27,7 +27,7 @@ import java.util.Set;
  * Created by ashvayka on 19.03.18.
  */
 @Data
-final class RuleNodeToRuleChainTellNextMsg implements TbActorMsg {
+class RuleNodeToRuleChainTellNextMsg implements TbActorMsg {
 
     private final RuleNodeId originator;
     private final Set<String> relationTypes;
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index 7a3127d..b4ab0d2 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.rule.RuleChain;
 import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
+import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import scala.concurrent.duration.Duration;
@@ -94,7 +95,8 @@ public class TenantActor extends RuleChainManagerActor {
                 onToDeviceActorMsg((DeviceAwareMsg) msg);
                 break;
             case RULE_CHAIN_TO_RULE_CHAIN_MSG:
-                onRuleChainMsg((RuleChainToRuleChainMsg) msg);
+            case REMOTE_TO_RULE_CHAIN_TELL_NEXT_MSG:
+                onRuleChainMsg((RuleChainAwareMsg) msg);
                 break;
             default:
                 return false;
@@ -109,15 +111,19 @@ public class TenantActor extends RuleChainManagerActor {
     }
 
     private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
+    	if (ruleChainManager.getRootChainActor()!=null)
         ruleChainManager.getRootChainActor().tell(msg, self());
+    	else logger.info("[{}] No Root Chain", msg);
     }
 
     private void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg msg) {
+    	if (ruleChainManager.getRootChainActor()!=null)
         ruleChainManager.getRootChainActor().tell(msg, self());
+    	else logger.info("[{}] No Root Chain", msg);
     }
 
-    private void onRuleChainMsg(RuleChainToRuleChainMsg msg) {
-        ruleChainManager.getOrCreateActor(context(), msg.getTarget()).tell(msg, self());
+    private void onRuleChainMsg(RuleChainAwareMsg msg) {
+        ruleChainManager.getOrCreateActor(context(), msg.getRuleChainId()).tell(msg, self());
     }
 
 
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 28cc2fd..51ef566 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.alarm.Alarm;
 import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.alarm.AlarmInfo;
@@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery;
 import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
 import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.EntityId;
@@ -53,7 +55,6 @@ public class AlarmController extends BaseController {
         checkParameter(ALARM_ID, strAlarmId);
         try {
             AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
-
             return checkAlarmId(alarmId);
         } catch (Exception e) {
             throw handleException(e);
@@ -79,8 +80,14 @@ public class AlarmController extends BaseController {
     public Alarm saveAlarm(@RequestBody Alarm alarm) throws ThingsboardException {
         try {
             alarm.setTenantId(getCurrentUser().getTenantId());
-            return checkNotNull(alarmService.createOrUpdateAlarm(alarm));
+            Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm));
+            logEntityAction(savedAlarm.getId(), savedAlarm,
+                    getCurrentUser().getCustomerId(),
+                    savedAlarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+            return savedAlarm;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.ASSET), alarm,
+                    null, alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
             throw handleException(e);
         }
     }
@@ -92,8 +99,9 @@ public class AlarmController extends BaseController {
         checkParameter(ALARM_ID, strAlarmId);
         try {
             AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
-            checkAlarmId(alarmId);
+            Alarm alarm = checkAlarmId(alarmId);
             alarmService.ackAlarm(alarmId, System.currentTimeMillis()).get();
+            logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -106,8 +114,9 @@ public class AlarmController extends BaseController {
         checkParameter(ALARM_ID, strAlarmId);
         try {
             AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
-            checkAlarmId(alarmId);
+            Alarm alarm = checkAlarmId(alarmId);
             alarmService.clearAlarm(alarmId, null, System.currentTimeMillis()).get();
+            logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
         } catch (Exception e) {
             throw handleException(e);
         }
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index f044228..de73fe0 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -529,18 +529,16 @@ public abstract class BaseController {
         return baseUrl;
     }
 
-    protected <I extends UUIDBased & EntityId> I emptyId(EntityType entityType) {
+    protected <I extends EntityId> I emptyId(EntityType entityType) {
         return (I)EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
     }
 
-    protected <E extends BaseData<I> & HasName,
-            I extends UUIDBased & EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
+    protected <E extends HasName, I extends EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
                                                                  ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
         logEntityAction(getCurrentUser(), entityId, entity, customerId, actionType, e, additionalInfo);
     }
 
-    protected <E extends BaseData<I> & HasName,
-            I extends UUIDBased & EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
+    protected <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
                                                                  ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
         if (customerId == null || customerId.isNullUid()) {
             customerId = user.getCustomerId();
@@ -556,8 +554,7 @@ public abstract class BaseController {
         return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
     }
 
-    private <E extends BaseData<I> & HasName,
-            I extends UUIDBased & EntityId> void pushEntityActionToRuleEngine(I entityId, E entity, User user, CustomerId customerId,
+    private <E extends HasName, I extends EntityId> void pushEntityActionToRuleEngine(I entityId, E entity, User user, CustomerId customerId,
                                                                          ActionType actionType, Object... additionalInfo) {
         String msgType = null;
         switch (actionType) {
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 844dbd3..70dd8a6 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -24,10 +24,13 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntityRelationInfo;
 import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
@@ -58,7 +61,15 @@ public class EntityRelationController extends BaseController {
                 relation.setTypeGroup(RelationTypeGroup.COMMON);
             }
             relationService.saveRelation(relation);
+            logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_ADD_OR_UPDATE, null, relation);
+            logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_ADD_OR_UPDATE, null, relation);
         } catch (Exception e) {
+            logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_ADD_OR_UPDATE, e, relation);
+            logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_ADD_OR_UPDATE, e, relation);
             throw handleException(e);
         }
     }
@@ -81,12 +92,21 @@ public class EntityRelationController extends BaseController {
         checkEntityId(fromId);
         checkEntityId(toId);
         RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
+        EntityRelation relation = new EntityRelation(fromId, toId, strRelationType, relationTypeGroup);
         try {
             Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup);
             if (!found) {
                 throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
             }
+            logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_DELETED, null, relation);
+            logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_DELETED, null, relation);
         } catch (Exception e) {
+            logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_DELETED, e, relation);
+            logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
+                    ActionType.RELATION_DELETED, e, relation);
             throw handleException(e);
         }
     }
@@ -102,7 +122,9 @@ public class EntityRelationController extends BaseController {
         checkEntityId(entityId);
         try {
             relationService.deleteEntityRelations(entityId);
+            logEntityAction(entityId, null, getCurrentUser().getCustomerId(), ActionType.RELATIONS_DELETED, null);
         } catch (Exception e) {
+            logEntityAction(entityId, null, getCurrentUser().getCustomerId(), ActionType.RELATIONS_DELETED, e);
             throw handleException(e);
         }
     }
@@ -210,8 +232,8 @@ public class EntityRelationController extends BaseController {
     @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {TO_ID, TO_TYPE})
     @ResponseBody
     public List<EntityRelationInfo> findInfoByTo(@RequestParam(TO_ID) String strToId,
-                                                   @RequestParam(TO_TYPE) String strToType,
-                                                   @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
+                                                 @RequestParam(TO_TYPE) String strToType,
+                                                 @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter(TO_ID, strToId);
         checkParameter(TO_TYPE, strToType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
@@ -276,10 +298,11 @@ public class EntityRelationController extends BaseController {
 
     private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
         RelationTypeGroup result = defaultValue;
-        if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) {
+        if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length() > 0) {
             try {
                 result = RelationTypeGroup.valueOf(strRelationTypeGroup);
-            } catch (IllegalArgumentException e) { }
+            } catch (IllegalArgumentException e) {
+            }
         }
         return result;
     }
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index b771ee2..136bbfd 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -46,7 +46,6 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
     private static final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
 
     @Autowired
-    @Lazy
     private TelemetryWebSocketService webSocketService;
 
     @Override
diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
index 8e86661..64f5213 100644
--- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.protobuf.InvalidProtocolBufferException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.thingsboard.rule.engine.api.RpcError;
 import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
@@ -67,6 +68,7 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
     private ClusterRpcService rpcService;
 
     @Autowired
+    @Lazy
     private ActorService actorService;
 
     private ScheduledExecutorService rpcCallBackExecutor;
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
index a41fdee..f4e37db 100644
--- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
@@ -28,6 +28,7 @@ import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.thingsboard.rule.engine.api.RpcError;
 import org.thingsboard.server.actors.service.ActorService;
@@ -102,6 +103,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
     private AttributesService attributesService;
 
     @Autowired
+    @Lazy
     private ActorService actorService;
 
     @Autowired
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index ef1606a..263b484 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -432,7 +432,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
             deviceSubscriptions.stream().filter(filter).forEach(s -> {
                 String sessionId = s.getWsSessionId();
                 List<TsKvEntry> subscriptionUpdate = f.apply(s);
-                if (subscriptionUpdate == null || !subscriptionUpdate.isEmpty()) {
+                if (subscriptionUpdate != null && !subscriptionUpdate.isEmpty()) {
                     SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate);
                     if (s.isLocal()) {
                         updateSubscriptionState(sessionId, s, update);
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 5653193..9f6192b 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -225,7 +225,7 @@ sql:
 # Actor system parameters
 actors:
   tenant:
-    create_components_on_init: true
+    create_components_on_init: "${ACTORS_TENANT_CREATE_COMPONENTS_ON_INIT:true}"
   session:
     max_concurrent_sessions_per_device: "${ACTORS_MAX_CONCURRENT_SESSION_PER_DEVICE:1}"
     sync:
@@ -328,6 +328,13 @@ spring.mvc.cors:
          max-age: "1800"
          allow-credentials: "true"
 
+# spring serve gzip compressed static resources
+spring.resources.chain:
+  gzipped: "true"
+  strategy:
+    content:
+      enabled: "true"
+
 # HSQLDB DAO Configuration
 spring:
   data:
@@ -378,6 +385,7 @@ audit_log:
       "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}"
       "user": "${AUDIT_LOG_MASK_USER:W}"
       "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}"
+      "alarm": "${AUDIT_LOG_MASK_ALARM:W}"
   sink:
     # Type of external sink. possible options: none, elasticsearch
     type: "${AUDIT_LOG_SINK_TYPE:none}"
diff --git a/application/src/main/scripts/install/install_dev_db.sh b/application/src/main/scripts/install/install_dev_db.sh
index ba93479..212a2ac 100644
--- a/application/src/main/scripts/install/install_dev_db.sh
+++ b/application/src/main/scripts/install/install_dev_db.sh
@@ -30,13 +30,13 @@ export SQL_DATA_FOLDER=${SQL_DATA_FOLDER:-/tmp}
 
 run_user=thingsboard
 
-su -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
+sudo -u "$run_user" -s /bin/sh -c "java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
                     -Dinstall.data_dir=${installDir} \
                     -Dinstall.load_demo=${loadDemo} \
                     -Dspring.jpa.hibernate.ddl-auto=none \
                     -Dinstall.upgrade=false \
                     -Dlogging.config=logback.xml \
-                    org.springframework.boot.loader.PropertiesLauncher" "$run_user"
+                    org.springframework.boot.loader.PropertiesLauncher"
 
 if [ $? -ne 0 ]; then
     echo "ThingsBoard DB installation failed!"
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 7953b48..1cbd1b5 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
index ea442f0..c37d460 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
@@ -31,7 +31,12 @@ public enum ActionType {
     ACTIVATED(false), // log string id
     SUSPENDED(false), // log string id
     CREDENTIALS_READ(true), // log device id
-    ATTRIBUTES_READ(true); // log attributes
+    ATTRIBUTES_READ(true), // log attributes
+    RELATION_ADD_OR_UPDATE (false),
+    RELATION_DELETED (false),
+    RELATIONS_DELETED (false),
+    ALARM_ACK (false),
+    ALARM_CLEAR (false);
 
     private final boolean isRead;
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index 0ecc7c6..ed4cf2f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -60,4 +60,5 @@ public class EntityIdFactory {
         }
         throw new IllegalArgumentException("EntityType " + type + " is not supported!");
     }
+
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java
index 2e976f2..beccc04 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java
@@ -33,13 +33,19 @@ public class RelationsSearchParameters {
     private UUID rootId;
     private EntityType rootType;
     private EntitySearchDirection direction;
+    private RelationTypeGroup relationTypeGroup;
     private int maxLevel = 1;
 
     public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel) {
+        this(entityId, direction, maxLevel, RelationTypeGroup.COMMON);
+    }
+
+    public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel, RelationTypeGroup relationTypeGroup) {
         this.rootId = entityId.getId();
         this.rootType = entityId.getEntityType();
         this.direction = direction;
         this.maxLevel = maxLevel;
+        this.relationTypeGroup = relationTypeGroup;
     }
 
     public EntityId getEntityId() {
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 91e617e..e1af6d1 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java
new file mode 100644
index 0000000..e261cbb
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.RuleChainId;
+
+public interface RuleChainAwareMsg {
+
+	RuleChainId getRuleChainId();
+	
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
index 7702788..c8b5c4e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
@@ -63,6 +63,11 @@ public enum MsgType {
     RULE_TO_RULE_CHAIN_TELL_NEXT_MSG,
 
     /**
+     * Message forwarded from original rule chain to remote rule chain due to change in the cluster structure or originator entity of the TbMsg.
+     */
+    REMOTE_TO_RULE_CHAIN_TELL_NEXT_MSG,
+
+    /**
      * Message that is sent by RuleActor implementation to RuleActor itself to log the error.
      */
     RULE_TO_SELF_ERROR_MSG,
@@ -101,6 +106,10 @@ public enum MsgType {
     /**
      * Message that is sent from Rule Engine to the Device Actor when message is successfully pushed to queue.
      */
-    RULE_ENGINE_QUEUE_PUT_ACK_MSG, ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG, TRANSPORT_TO_DEVICE_SESSION_ACTOR_MSG, SESSION_TIMEOUT_MSG, SESSION_CTRL_MSG;
+    RULE_ENGINE_QUEUE_PUT_ACK_MSG,
+    ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG,
+    TRANSPORT_TO_DEVICE_SESSION_ACTOR_MSG,
+    SESSION_TIMEOUT_MSG,
+    SESSION_CTRL_MSG;
 
 }

common/pom.xml 2(+1 -1)

diff --git a/common/pom.xml b/common/pom.xml
index 4f555e0..8c2416d 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index b7a4255..07c4d34 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>

dao/pom.xml 2(+1 -1)

diff --git a/dao/pom.xml b/dao/pom.xml
index f6050d1..00173bd 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <artifactId>dao</artifactId>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
index d2e6f05..b9db338 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
@@ -40,15 +40,14 @@ public interface AuditLogService {
 
     TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink);
 
-    <E extends BaseData<I> & HasName,
-            I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(
-                                                        TenantId tenantId,
-                                                        CustomerId customerId,
-                                                        UserId userId,
-                                                        String userName,
-                                                        I entityId,
-                                                        E entity,
-                                                        ActionType actionType,
-                                                        Exception e, Object... additionalInfo);
+    <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction(
+            TenantId tenantId,
+            CustomerId customerId,
+            UserId userId,
+            String userName,
+            I entityId,
+            E entity,
+            ActionType actionType,
+            Exception e, Object... additionalInfo);
 
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index a30c1b4..ecb2bd5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 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.rule.RuleChainMetaData;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.audit.sink.AuditLogSink;
@@ -115,7 +116,7 @@ public class AuditLogServiceImpl implements AuditLogService {
     }
 
     @Override
-    public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>>
+    public <E extends HasName, I extends EntityId> ListenableFuture<List<Void>>
         logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,
                                ActionType actionType, Exception e, Object... additionalInfo) {
         if (canLog(entityId.getEntityType(), actionType)) {
@@ -156,14 +157,16 @@ public class AuditLogServiceImpl implements AuditLogService {
         }
     }
 
-    private <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> JsonNode constructActionData(I entityId,
-                                                                                                           E entity,
+    private <E extends HasName, I extends EntityId> JsonNode constructActionData(I entityId, E entity,
                                                                                                            ActionType actionType,
                                                                                                            Object... additionalInfo) {
         ObjectNode actionData = objectMapper.createObjectNode();
         switch(actionType) {
             case ADDED:
             case UPDATED:
+            case ALARM_ACK:
+            case ALARM_CLEAR:
+            case RELATIONS_DELETED:
                 if (entity != null) {
                     ObjectNode entityNode = objectMapper.valueToTree(entity);
                     if (entityId.getEntityType() == EntityType.DASHBOARD) {
@@ -240,6 +243,11 @@ public class AuditLogServiceImpl implements AuditLogService {
                 actionData.put("unassignedCustomerId", strCustomerId);
                 actionData.put("unassignedCustomerName", strCustomerName);
                 break;
+            case RELATION_ADD_OR_UPDATE:
+            case RELATION_DELETED:
+                EntityRelation relation = extractParameter(EntityRelation.class, 0, additionalInfo);
+                actionData.set("relation", objectMapper.valueToTree(relation));
+                break;
         }
         return actionData;
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
index b14d6b1..6f9ea36 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
@@ -57,7 +57,7 @@ public class DummyAuditLogServiceImpl implements AuditLogService {
     }
 
     @Override
-    public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
+    public <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
         return null;
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java
index 2dc624f..688dfb3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java
@@ -16,8 +16,10 @@
 package org.thingsboard.server.dao.cache;
 
 import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.RemovalCause;
 import com.github.benmanes.caffeine.cache.Ticker;
 import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.cache.CacheManager;
@@ -28,6 +30,7 @@ import org.springframework.cache.support.SimpleCacheManager;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -38,12 +41,14 @@ import java.util.stream.Collectors;
 @ConfigurationProperties(prefix = "caffeine")
 @EnableCaching
 @Data
+@Slf4j
 public class CaffeineCacheConfiguration {
 
     private Map<String, CacheSpecs> specs;
 
     @Bean
     public CacheManager cacheManager() {
+        log.trace("Initializing cache: {}", Arrays.toString(RemovalCause.values()));
         SimpleCacheManager manager = new SimpleCacheManager();
         if (specs != null) {
             List<CaffeineCache> caches =
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index ea29f6f..9b89fe2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -402,7 +402,7 @@ public class BaseRelationService implements RelationService {
         int maxLvl = params.getMaxLevel() > 0 ? params.getMaxLevel() : Integer.MAX_VALUE;
 
         try {
-            ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), maxLvl, new ConcurrentHashMap<>());
+            ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), params.getRelationTypeGroup(), maxLvl, new ConcurrentHashMap<>());
             return Futures.transform(relationSet, input -> {
                 List<EntityRelation> relations = new ArrayList<>();
                 if (filters == null || filters.isEmpty()) {
@@ -518,14 +518,15 @@ public class BaseRelationService implements RelationService {
         }
     }
 
-    private ListenableFuture<Set<EntityRelation>> findRelationsRecursively(final EntityId rootId, final EntitySearchDirection direction, int lvl,
+    private ListenableFuture<Set<EntityRelation>> findRelationsRecursively(final EntityId rootId, final EntitySearchDirection direction,
+                                                                           RelationTypeGroup relationTypeGroup, int lvl,
                                                                            final ConcurrentHashMap<EntityId, Boolean> uniqueMap) throws Exception {
         if (lvl == 0) {
             return Futures.immediateFuture(Collections.emptySet());
         }
         lvl--;
         //TODO: try to remove this blocking operation
-        Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
+        Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction, relationTypeGroup).get());
         Set<EntityId> childrenIds = new HashSet<>();
         for (EntityRelation childRelation : children) {
             log.trace("Found Relation: {}", childRelation);
@@ -544,7 +545,7 @@ public class BaseRelationService implements RelationService {
         }
         List<ListenableFuture<Set<EntityRelation>>> futures = new ArrayList<>();
         for (EntityId entityId : childrenIds) {
-            futures.add(findRelationsRecursively(entityId, direction, lvl, uniqueMap));
+            futures.add(findRelationsRecursively(entityId, direction, relationTypeGroup, lvl, uniqueMap));
         }
         //TODO: try to remove this blocking operation
         List<Set<EntityRelation>> relations = Futures.successfulAsList(futures).get();
@@ -552,12 +553,15 @@ public class BaseRelationService implements RelationService {
         return Futures.immediateFuture(children);
     }
 
-    private ListenableFuture<List<EntityRelation>> findRelations(final EntityId rootId, final EntitySearchDirection direction) {
+    private ListenableFuture<List<EntityRelation>> findRelations(final EntityId rootId, final EntitySearchDirection direction, RelationTypeGroup relationTypeGroup) {
         ListenableFuture<List<EntityRelation>> relations;
+        if (relationTypeGroup == null) {
+            relationTypeGroup = RelationTypeGroup.COMMON;
+        }
         if (direction == EntitySearchDirection.FROM) {
-            relations = findByFromAsync(rootId, RelationTypeGroup.COMMON);
+            relations = findByFromAsync(rootId, relationTypeGroup);
         } else {
-            relations = findByToAsync(rootId, RelationTypeGroup.COMMON);
+            relations = findByToAsync(rootId, relationTypeGroup);
         }
         return relations;
     }
diff --git a/docker/cassandra/Makefile b/docker/cassandra/Makefile
index 29941f5..d1fb677 100644
--- a/docker/cassandra/Makefile
+++ b/docker/cassandra/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.0.3
+VERSION=2.1.0
 PROJECT=thingsboard
 APP=cassandra
 
diff --git a/docker/cassandra-setup/Makefile b/docker/cassandra-setup/Makefile
index e0bb541..7990a3e 100644
--- a/docker/cassandra-setup/Makefile
+++ b/docker/cassandra-setup/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.0.3
+VERSION=2.1.0
 PROJECT=thingsboard
 APP=cassandra-setup
 
diff --git a/docker/cassandra-upgrade/Dockerfile b/docker/cassandra-upgrade/Dockerfile
new file mode 100644
index 0000000..312db0d
--- /dev/null
+++ b/docker/cassandra-upgrade/Dockerfile
@@ -0,0 +1,24 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM openjdk:8-jre
+
+ADD upgrade.sh /upgrade.sh
+ADD thingsboard.deb /thingsboard.deb
+
+RUN apt-get update \
+        && apt-get install -y nmap \
+        && chmod +x /upgrade.sh
diff --git a/docker/cassandra-upgrade/Makefile b/docker/cassandra-upgrade/Makefile
new file mode 100644
index 0000000..b2e8501
--- /dev/null
+++ b/docker/cassandra-upgrade/Makefile
@@ -0,0 +1,12 @@
+VERSION=2.1.0
+PROJECT=thingsboard
+APP=cassandra-upgrade
+
+build:
+	cp ../../application/target/thingsboard.deb .
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} -t ${PROJECT}/${APP}:latest .
+	rm thingsboard.deb
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
+	docker push ${PROJECT}/${APP}:latest
diff --git a/docker/cassandra-upgrade/upgrade.sh b/docker/cassandra-upgrade/upgrade.sh
new file mode 100755
index 0000000..dac4919
--- /dev/null
+++ b/docker/cassandra-upgrade/upgrade.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+dpkg -i /thingsboard.deb
+
+until nmap $CASSANDRA_HOST -p $CASSANDRA_PORT | grep "$CASSANDRA_PORT/tcp open"
+do
+  echo "Wait for cassandra db to start..."
+  sleep 10
+done
+
+echo "Upgrading 'Thingsboard' schema..."
+/usr/share/thingsboard/bin/install/upgrade.sh --fromVersion=$UPGRADE_FROM_VERSION
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 4b34e3a..89a8369 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -18,7 +18,7 @@ version: '2'
 
 services:
   tb:
-    image: "thingsboard/application:2.0.3"
+    image: "thingsboard/application:2.1.0"
     ports:
       - "8080:8080"
       - "1883:1883"
diff --git a/docker/k8s/cassandra.yaml b/docker/k8s/cassandra.yaml
index 14ac8aa..3165bb1 100644
--- a/docker/k8s/cassandra.yaml
+++ b/docker/k8s/cassandra.yaml
@@ -54,7 +54,7 @@ spec:
               topologyKey: "kubernetes.io/hostname"
       containers:
       - name: cassandra
-        image: thingsboard/cassandra:2.0.3
+        image: thingsboard/cassandra:2.1.0
         imagePullPolicy: Always
         ports:
         - containerPort: 7000
diff --git a/docker/k8s/cassandra-setup.yaml b/docker/k8s/cassandra-setup.yaml
index f3d2a43..381df77 100644
--- a/docker/k8s/cassandra-setup.yaml
+++ b/docker/k8s/cassandra-setup.yaml
@@ -22,7 +22,7 @@ spec:
   containers:
   - name: cassandra-setup
     imagePullPolicy: Always
-    image: thingsboard/cassandra-setup:2.0.3
+    image: thingsboard/cassandra-setup:2.1.0
     env:
     - name: ADD_DEMO_DATA
       value: "true"
diff --git a/docker/k8s/cassandra-upgrade.yaml b/docker/k8s/cassandra-upgrade.yaml
new file mode 100644
index 0000000..a78136e
--- /dev/null
+++ b/docker/k8s/cassandra-upgrade.yaml
@@ -0,0 +1,43 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: Pod
+metadata:
+  name: cassandra-upgrade
+spec:
+  containers:
+  - name: cassandra-upgrade
+    imagePullPolicy: Always
+    image: thingsboard/cassandra-upgrade:2.1.0
+    env:
+    - name: ADD_DEMO_DATA
+      value: "true"
+    - name : CASSANDRA_HOST
+      value: "cassandra-headless"
+    - name : CASSANDRA_PORT
+      value: "9042"
+    - name : DATABASE_TYPE
+      value: "cassandra"
+    - name : CASSANDRA_URL
+      value: "cassandra-headless:9042"
+    - name : UPGRADE_FROM_VERSION
+      value: "1.4.0"
+    command:
+    - sh
+    - -c
+    - /upgrade.sh
+  restartPolicy: Never
diff --git a/docker/k8s/tb.yaml b/docker/k8s/tb.yaml
index 8f507e1..f38e1f1 100644
--- a/docker/k8s/tb.yaml
+++ b/docker/k8s/tb.yaml
@@ -84,7 +84,7 @@ spec:
       containers:
       - name: tb
         imagePullPolicy: Always
-        image: thingsboard/application:2.0.3
+        image: thingsboard/application:2.1.0
         ports:
         - containerPort: 8080
           name: ui
diff --git a/docker/k8s/zookeeper.yaml b/docker/k8s/zookeeper.yaml
index f9948a8..ae33ea2 100644
--- a/docker/k8s/zookeeper.yaml
+++ b/docker/k8s/zookeeper.yaml
@@ -87,7 +87,7 @@ spec:
       containers:
       - name: zk
         imagePullPolicy: Always
-        image: thingsboard/zk:2.0.3
+        image: thingsboard/zk:2.1.0
         ports:
         - containerPort: 2181
           name: client
diff --git a/docker/tb/Makefile b/docker/tb/Makefile
index 90793ed..96803ab 100644
--- a/docker/tb/Makefile
+++ b/docker/tb/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.0.3
+VERSION=2.1.0
 PROJECT=thingsboard
 APP=application
 
diff --git a/docker/zookeeper/Makefile b/docker/zookeeper/Makefile
index c96a6ea..5c58a74 100644
--- a/docker/zookeeper/Makefile
+++ b/docker/zookeeper/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.0.3
+VERSION=2.1.0
 PROJECT=thingsboard
 APP=zk
 
diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml
index a86fc58..e5904fd 100644
--- a/netty-mqtt/pom.xml
+++ b/netty-mqtt/pom.xml
@@ -19,12 +19,12 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
     <artifactId>netty-mqtt</artifactId>
-    <version>2.0.3</version>
+    <version>2.1.0-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>Netty MQTT Client</name>

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index e8ed5ad..4be0a7f 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.thingsboard</groupId>
     <artifactId>thingsboard</artifactId>
-    <version>2.0.3</version>
+    <version>2.1.0-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <name>Thingsboard</name>
diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
index f53eadf..7d769ce 100644
--- a/rule-engine/pom.xml
+++ b/rule-engine/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <artifactId>rule-engine</artifactId>
diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml
index f067865..fce5562 100644
--- a/rule-engine/rule-engine-api/pom.xml
+++ b/rule-engine/rule-engine-api/pom.xml
@@ -22,7 +22,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>rule-engine</artifactId>
     </parent>
     <groupId>org.thingsboard.rule-engine</groupId>
diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index 1759921..aed7efd 100644
--- a/rule-engine/rule-engine-components/pom.xml
+++ b/rule-engine/rule-engine-components/pom.xml
@@ -22,7 +22,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>rule-engine</artifactId>
     </parent>
     <groupId>org.thingsboard.rule-engine</groupId>
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
index 5a30dcb..e7a54af 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
@@ -47,11 +47,12 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
 
 public class TbMsgGeneratorNode implements TbNode {
 
-    public static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
+    private static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
 
     private TbMsgGeneratorNodeConfiguration config;
     private ScriptEngine jsEngine;
     private long delay;
+    private long lastScheduledTs;
     private EntityId originatorId;
     private UUID nextTickId;
     private TbMsg prevMsg;
@@ -66,28 +67,40 @@ public class TbMsgGeneratorNode implements TbNode {
             originatorId = ctx.getSelfId();
         }
         this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
-        sentTickMsg(ctx);
+        scheduleTickMsg(ctx);
     }
 
     @Override
     public void onMsg(TbContext ctx, TbMsg msg) {
         if (msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
             withCallback(generate(ctx),
-                    m -> {ctx.tellNext(m, SUCCESS); sentTickMsg(ctx);},
-                    t -> {ctx.tellFailure(msg, t); sentTickMsg(ctx);});
+                    m -> {
+                        ctx.tellNext(m, SUCCESS);
+                        scheduleTickMsg(ctx);
+                    },
+                    t -> {
+                        ctx.tellFailure(msg, t);
+                        scheduleTickMsg(ctx);
+                    });
         }
     }
 
-    private void sentTickMsg(TbContext ctx) {
+    private void scheduleTickMsg(TbContext ctx) {
+        long curTs = System.currentTimeMillis();
+        if (lastScheduledTs == 0L) {
+            lastScheduledTs = curTs;
+        }
+        lastScheduledTs = lastScheduledTs + delay;
+        long curDelay = Math.max(0L, (lastScheduledTs - curTs));
         TbMsg tickMsg = ctx.newMsg(TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
         nextTickId = tickMsg.getId();
-        ctx.tellSelf(tickMsg, delay);
+        ctx.tellSelf(tickMsg, curDelay);
     }
 
     private ListenableFuture<TbMsg> generate(TbContext ctx) {
         return ctx.getJsExecutor().executeAsync(() -> {
             if (prevMsg == null) {
-                prevMsg = ctx.newMsg( "", originatorId, new TbMsgMetaData(), "{}");
+                prevMsg = ctx.newMsg("", originatorId, new TbMsgMetaData(), "{}");
             }
             TbMsg generated = jsEngine.executeGenerate(prevMsg);
             prevMsg = ctx.newMsg(generated.getType(), originatorId, generated.getMetaData(), generated.getData());
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java
new file mode 100644
index 0000000..702e10f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.delay;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "delay",
+        configClazz = TbMsgDelayNodeConfiguration.class,
+        nodeDescription = "Delays incoming message",
+        nodeDetails = "Delays messages for configurable period.",
+        icon = "pause",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeMsgDelayConfig"
+)
+
+public class TbMsgDelayNode implements TbNode {
+
+    private static final String TB_MSG_DELAY_NODE_MSG = "TbMsgDelayNodeMsg";
+
+    private TbMsgDelayNodeConfiguration config;
+    private long delay;
+    private Map<UUID, TbMsg> pendingMsgs;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgDelayNodeConfiguration.class);
+        this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
+        this.pendingMsgs = new HashMap<>();
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        if (msg.getType().equals(TB_MSG_DELAY_NODE_MSG)) {
+            TbMsg pendingMsg = pendingMsgs.remove(UUID.fromString(msg.getData()));
+            if (pendingMsg != null) {
+                ctx.tellNext(pendingMsg, SUCCESS);
+            }
+        } else {
+            if(pendingMsgs.size() < config.getMaxPendingMsgs()) {
+                pendingMsgs.put(msg.getId(), msg);
+                TbMsg tickMsg = ctx.newMsg(TB_MSG_DELAY_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), msg.getId().toString());
+                ctx.tellSelf(tickMsg, delay);
+            } else {
+                ctx.tellNext(msg, FAILURE, new RuntimeException("Max limit of pending messages reached!"));
+            }
+        }
+    }
+
+    @Override
+    public void destroy() {
+        pendingMsgs.clear();
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeConfiguration.java
new file mode 100644
index 0000000..411a1a5
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeConfiguration.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.delay;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.EntityType;
+
+@Data
+public class TbMsgDelayNodeConfiguration implements NodeConfiguration<TbMsgDelayNodeConfiguration> {
+
+    private int periodInSeconds;
+    private int maxPendingMsgs;
+
+    @Override
+    public TbMsgDelayNodeConfiguration defaultConfiguration() {
+        TbMsgDelayNodeConfiguration configuration = new TbMsgDelayNodeConfiguration();
+        configuration.setPeriodInSeconds(60);
+        configuration.setMaxPendingMsgs(1000);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java
new file mode 100644
index 0000000..043cb80
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "originator type",
+        configClazz = TbOriginatorTypeFilterNodeConfiguration.class,
+        relationTypes = {"True", "False"},
+        nodeDescription = "Filter incoming messages by message Originator Type",
+        nodeDetails = "If Originator Type of incoming message is expected - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbFilterNodeOriginatorTypeConfig")
+public class TbOriginatorTypeFilterNode implements TbNode {
+
+    TbOriginatorTypeFilterNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbOriginatorTypeFilterNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+        EntityType originatorType = msg.getOriginator().getEntityType();
+        ctx.tellNext(msg, config.getOriginatorTypes().contains(originatorType) ? "True" : "False");
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeConfiguration.java
new file mode 100644
index 0000000..83d0a99
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeConfiguration.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Data
+public class TbOriginatorTypeFilterNodeConfiguration implements NodeConfiguration<TbOriginatorTypeFilterNodeConfiguration> {
+
+    private List<EntityType> originatorTypes;
+
+    @Override
+    public TbOriginatorTypeFilterNodeConfiguration defaultConfiguration() {
+        TbOriginatorTypeFilterNodeConfiguration configuration = new TbOriginatorTypeFilterNodeConfiguration();
+        configuration.setOriginatorTypes(Arrays.asList(
+                EntityType.DEVICE
+        ));
+        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 c5e057e..e06d253 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,4 +1,4 @@
-!function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].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),a=e[t[0]];return function(e,t,r){a.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(72)},function(e,t){},1,1,1,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 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=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 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 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> "},21,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\">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 a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(5),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.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);a.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(i.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 i=n(6),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.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);a.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(i.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 i=n(7),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.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);a.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(i.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 i=n(8),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(51),i=a(r),o=n(36),l=a(o),s=n(39),u=a(s),d=n(38),c=a(d),m=n(37),g=a(m),p=n(42),f=a(p),b=n(46),v=a(b),y=n(47),q=a(y),h=n(45),T=a(h),$=n(41),k=a($),w=n(49),C=a(w),_=n(50),x=a(_),E=n(44),M=a(E),S=n(43),N=a(S),V=n(48),P=a(V);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.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",T.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",x.default).directive("tbActionNodeRabbitMqConfig",M.default).directive("tbActionNodeMqttConfig",N.default).directive("tbActionNodeSendEmailConfig",P.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(9),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(10),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var r=n.target.result;r&&r.length>0&&("caCert"==t&&(a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=r),"privateKey"==t&&(a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=r),"Cert"==t&&(a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=r)),a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null),"privateKey"==e&&(a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null),"Cert"==e&&(a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null),a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(2);var i=n(11),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(12),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(13),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(14),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(15),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(16),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(17),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.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 i=n(18),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(19),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(20),o=a(i)},function(e,t){"use strict";function n(e){var t=function(t,n,a,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 a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(21),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(22),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(57),i=a(r),o=n(58),l=a(o),s=n(55),u=a(s),d=n(59),c=a(d),m=n(54),g=a(m),p=n(60),f=a(p);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",u.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(23),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(24),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(25),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(26),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(27),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(64),i=a(r),o=n(63),l=a(o),s=n(65),u=a(s),d=n(61),c=a(d);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).directive("tbFilterNodeCheckRelationConfig",c.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<a.messageTypes.length;t++)e.push(a.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(a.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),a.selectedMessageType=null,a.messageTypeSearchText=null,a.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)}a.transformMessageTypeChip=function(e){
-var n,a=t("filter")(c,{name:e},!0);return n=a&&a.length?angular.copy(a[0]):{name:e,value:e}},a.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},a.createMessageType=function(e,t){var n=angular.element(t,r)[0].firstElementChild,a=angular.element(n),i=a.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),a.scope().$mdChipsCtrl.appendChip(i.trim()),a.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var r=0;r<e.messageTypes.length;r++){var i=e.messageTypes[r];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}a.messageTypes=t,a.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(3);var i=n(28),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(30),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){function i(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=i,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 i=n(31),o=a(i);n(4)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(32),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(33),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(68),i=a(r),o=n(70),l=a(o),s=n(71),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(34),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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 i=n(35),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(75),i=a(r),o=n(62),l=a(o),s=n(56),u=a(s),d=n(69),c=a(d),m=n(40),g=a(m),p=n(53),f=a(p),b=n(67),v=a(b),y=n(52),q=a(y),h=n(66),T=a(h),$=n(74),k=a($);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",T.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{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"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){(0,o.default)(t);for(var n in t){var a=t[n];e.translations(n,a)}}r.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(73),o=a(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES_REQUEST:{name:"Post attributes",value:"POST_ATTRIBUTES_REQUEST"},POST_TELEMETRY_REQUEST:{name:"Post telemetry",value:"POST_TELEMETRY_REQUEST"},TO_SERVER_RPC_REQUEST:{name:"RPC Request from Device",value:"TO_SERVER_RPC_REQUEST"},RPC_CALL_FROM_SERVER_TO_DEVICE:{name:"RPC Request to Device",value:"RPC_CALL_FROM_SERVER_TO_DEVICE"},ACTIVITY_EVENT:{name:"Activity Event",value:"ACTIVITY_EVENT"},INACTIVITY_EVENT:{name:"Inactivity Event",value:"INACTIVITY_EVENT"},CONNECT_EVENT:{name:"Connect Event",value:"CONNECT_EVENT"},DISCONNECT_EVENT:{name:"Disconnect Event",value:"DISCONNECT_EVENT"},ENTITY_CREATED:{name:"Entity Created",value:"ENTITY_CREATED"},ENTITY_UPDATED:{name:"Entity Updated",value:"ENTITY_UPDATED"},ENTITY_DELETED:{name:"Entity Deleted",value:"ENTITY_DELETED"},ENTITY_ASSIGNED:{name:"Entity Assigned",value:"ENTITY_ASSIGNED"},ENTITY_UNASSIGNED:{name:"Entity Unassigned",value:"ENTITY_UNASSIGNED"},ATTRIBUTES_UPDATED:{name:"Attributes Updated",value:"ATTRIBUTES_UPDATED"},ATTRIBUTES_DELETED:{name:"Attributes Deleted",value:"ATTRIBUTES_DELETED"}},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"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},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(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].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),r=e[t[0]];return function(e,t,a){r.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(76)},function(e,t){},1,1,1,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 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=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 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 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> "},22,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 r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(5),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.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);r.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(i.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 i=n(6),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.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);r.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(i.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 i=n(7),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.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);r.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(i.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 i=n(8),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(54),i=r(a),o=n(38),l=r(o),s=n(41),u=r(s),d=n(40),c=r(d),m=n(39),g=r(m),p=n(44),f=r(p),b=n(49),v=r(b),y=n(50),q=r(y),h=n(48),$=r(h),k=n(43),T=r(k),w=n(52),x=r(w),C=n(53),M=r(C),_=n(47),S=r(_),N=n(45),V=r(N),j=n(51),P=r(j),F=n(46),E=r(F);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.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",x.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",V.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",E.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(9),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(10),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$mdExpansionPanel=t,r.ruleNodeTypes=n,r.credentialsTypeChanged=function(){var e=r.configuration.credentials.type;r.configuration.credentials={},r.configuration.credentials.type=e,r.updateValidity()},r.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){r.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(r.configuration.credentials.caCertFileName=e.name,r.configuration.credentials.caCert=a),"privateKey"==t&&(r.configuration.credentials.privateKeyFileName=e.name,r.configuration.credentials.privateKey=a),"Cert"==t&&(r.configuration.credentials.certFileName=e.name,r.configuration.credentials.cert=a)),r.updateValidity()}})},n.readAsText(e.file)},r.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(r.configuration.credentials.caCertFileName=null,r.configuration.credentials.caCert=null),"privateKey"==e&&(r.configuration.credentials.privateKeyFileName=null,r.configuration.credentials.privateKey=null),"Cert"==e&&(r.configuration.credentials.certFileName=null,r.configuration.credentials.cert=null),r.updateValidity()},r.updateValidity=function(){var e=!0,t=r.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var i=n(11),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(12),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(13),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(14),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(15),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(16),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(17),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(18),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.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 i=n(19),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(20),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(21),o=r(i)},function(e,t){"use strict";function n(e){var t=function(t,n,r,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 r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(22),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(23),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(60),i=r(a),o=n(61),l=r(o),s=n(58),u=r(s),d=n(62),c=r(d),m=n(57),g=r(m),p=n(63),f=r(p);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",u.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(24),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(25),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(26),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(27),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(28),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(68),i=r(a),o=n(66),l=r(o),s=n(69),u=r(s),d=n(64),c=r(d),m=n(67),g=r(m);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.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 r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<r.messageTypes.length;t++)e.push(r.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(r.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),r.selectedMessageType=null,r.messageTypeSearchText=null,r.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)}r.transformMessageTypeChip=function(e){var n,r=t("filter")(c,{name:e},!0);return n=r&&r.length?angular.copy(r[0]):{name:e,value:e}},r.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},r.createMessageType=function(e,t){var n=angular.element(t,a)[0].firstElementChild,r=angular.element(n),i=r.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),r.scope().$mdChipsCtrl.appendChip(i.trim()),r.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){r.messageTypesWatch&&(r.messageTypesWatch(),r.messageTypesWatch=null);var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var a=0;a<e.messageTypes.length;a++){var i=e.messageTypes[a];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}r.messageTypes=t,r.messageTypesWatch=r.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$filter","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(3);var i=n(29),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(30),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(31),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(32),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){function i(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=i,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 i=n(33),o=r(i);n(4)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(34),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(35),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(72),i=r(a),o=n(74),l=r(o),s=n(75),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(36),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(37),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(79),i=r(a),o=n(65),l=r(o),s=n(59),u=r(s),d=n(73),c=r(d),m=n(42),g=r(m),p=n(56),f=r(p),b=n(71),v=r(b),y=n(55),q=r(y),h=n(70),$=r(h),k=n(78),T=r(k);t.default=angular.module("thingsboard.ruleChain.config",[i.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:{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"},"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 r(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 i=n(77),o=r(i)},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"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},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 2(+1 -1)

diff --git a/tools/pom.xml b/tools/pom.xml
index e5d443a..054eab0 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 9b37184..61e455a 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index d9c46d2..3978620 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index e0af4c0..0bdb9a8 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index cfba944..69ed17f 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.transport.mqtt.session;
 
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonSyntaxException;
 import io.netty.channel.ChannelHandlerContext;
@@ -240,7 +241,7 @@ public class GatewaySessionCtx {
 
     private String getDeviceType(JsonElement json) throws AdaptorException {
         JsonElement type = json.getAsJsonObject().get("type");
-        return type == null ? DEFAULT_DEVICE_TYPE : type.getAsString();
+        return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString();
     }
 
     private JsonElement getJson(MqttPublishMessage mqttMsg) throws AdaptorException {
diff --git a/transport/pom.xml b/transport/pom.xml
index bb1420f..03f3d11 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>

ui/package.json 22(+12 -10)

diff --git a/ui/package.json b/ui/package.json
index 328f47b..06b7fee 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
 {
   "name": "thingsboard",
   "private": true,
-  "version": "2.0.3",
+  "version": "2.1.0",
   "description": "Thingsboard UI",
   "licenses": [
     {
@@ -15,7 +15,6 @@
   },
   "dependencies": {
     "@flowjs/ng-flow": "^2.7.1",
-    "ace-builds": "1.3.1",
     "angular": "1.5.8",
     "angular-animate": "1.5.8",
     "angular-aria": "1.5.8",
@@ -37,17 +36,17 @@
     "angular-socialshare": "^2.3.8",
     "angular-storage": "0.0.15",
     "angular-touch": "1.5.8",
-    "angular-translate": "2.13.1",
-    "angular-translate-handler-log": "2.13.1",
-    "angular-translate-interpolation-messageformat": "2.13.1",
-    "angular-translate-loader-static-files": "2.13.1",
-    "angular-translate-storage-cookie": "2.13.1",
-    "angular-translate-storage-local": "2.13.1",
+    "angular-translate": "2.18.1",
+    "angular-translate-handler-log": "2.18.1",
+    "angular-translate-interpolation-messageformat": "2.18.1",
+    "angular-translate-loader-static-files": "2.18.1",
+    "angular-translate-storage-cookie": "2.18.1",
+    "angular-translate-storage-local": "2.18.1",
     "angular-ui-ace": "^0.2.3",
     "angular-ui-router": "^0.3.1",
     "angular-websocket": "^2.0.1",
     "base64-js": "^1.2.1",
-    "brace": "^0.8.0",
+    "brace": "^0.10.0",
     "canvas-gauges": "^2.0.9",
     "clipboard": "^1.5.15",
     "compass-sass-mixins": "^0.12.7",
@@ -96,6 +95,7 @@
     "babel-loader": "^6.2.5",
     "babel-preset-es2015": "^6.14.0",
     "babel-preset-react": "^6.16.0",
+    "compression-webpack-plugin": "^1.1.11",
     "connect-history-api-fallback": "^1.3.0",
     "copy-webpack-plugin": "^3.0.1",
     "cross-env": "^3.2.4",
@@ -127,7 +127,9 @@
     "webpack-dev-middleware": "^1.6.1",
     "webpack-dev-server": "^1.15.1",
     "webpack-hot-middleware": "^2.12.2",
-    "webpack-material-design-icons": "^0.1.0"
+    "webpack-material-design-icons": "^0.1.0",
+    "directory-tree": "^2.1.0",
+    "jsonminify": "^0.4.1"
   },
   "engine": "node >= 5.9.0",
   "nyc": {

ui/pom.xml 2(+1 -1)

diff --git a/ui/pom.xml b/ui/pom.xml
index b1c0919..8003eee 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>2.0.3</version>
+        <version>2.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/ui/src/app/api/alias-controller.js b/ui/src/app/api/alias-controller.js
index 10b1107..66781a8 100644
--- a/ui/src/app/api/alias-controller.js
+++ b/ui/src/app/api/alias-controller.js
@@ -146,6 +146,7 @@ export default class AliasController {
                                     newDatasource.entityId = resolvedEntity.id;
                                     newDatasource.entityType = resolvedEntity.entityType;
                                     newDatasource.entityName = resolvedEntity.name;
+                                    newDatasource.entityDescription = resolvedEntity.entityDescription
                                     newDatasource.name = resolvedEntity.name;
                                     newDatasource.generated = i > 0 ? true : false;
                                     datasources.push(newDatasource);
@@ -167,6 +168,7 @@ export default class AliasController {
                                 datasource.entityType = entity.entityType;
                                 datasource.entityName = entity.name;
                                 datasource.name = entity.name;
+                                datasource.entityDescription = entity.entityDescription;
                                 deferred.resolve([datasource]);
                             } else {
                                 if (aliasInfo.stateEntity) {
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
index 507fe97..adab48f 100644
--- a/ui/src/app/api/dashboard.service.js
+++ b/ui/src/app/api/dashboard.service.js
@@ -252,7 +252,7 @@ function DashboardService($rootScope, $http, $q, $location, $filter) {
         if (port != 80 && port != 443) {
             url += ":" + port;
         }
-        url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.publicCustomerId;
+        url += "/dashboard/" + dashboard.id.id + "?publicId=" + dashboard.publicCustomerId;
         return url;
     }
 
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index 762bf1a..2e29238 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -329,7 +329,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
     }
 
     function entityToEntityInfo(entity) {
-        return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id };
+        return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id, entityDescription: entity.additionalInfo?entity.additionalInfo.description:"" };
     }
 
     function entityRelationInfoToEntityInfo(entityRelationInfo, direction) {
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
index e7436de..186e31c 100644
--- a/ui/src/app/api/rule-chain.service.js
+++ b/ui/src/app/api/rule-chain.service.js
@@ -32,6 +32,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
         getRuleNodeComponents: getRuleNodeComponents,
         getRuleNodeComponentByClazz: getRuleNodeComponentByClazz,
         getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
+        ruleNodeAllowCustomLinks: ruleNodeAllowCustomLinks,
         resolveTargetRuleChains: resolveTargetRuleChains,
         testScript: testScript,
         getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput
@@ -127,21 +128,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
 
     function getRuleNodeSupportedLinks(component) {
         var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
-        var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
-        var linkLabels = [];
+        var linkLabels = {};
         for (var i=0;i<relationTypes.length;i++) {
-            linkLabels.push({
-                name: relationTypes[i], custom: false
-            });
-        }
-        if (customRelations) {
-            linkLabels.push(
-                { name: 'Custom', custom: true }
-            );
+            var label = relationTypes[i];
+            linkLabels[label] = {
+                name: label,
+                value: label
+            };
         }
         return linkLabels;
     }
 
+    function ruleNodeAllowCustomLinks(component) {
+        return component.configurationDescriptor.nodeDefinition.customRelations;
+    }
+
     function getRuleNodeComponents() {
         var deferred = $q.defer();
         if (ruleNodeComponents) {
@@ -226,7 +227,10 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
         if (res && res.length) {
             return res[0];
         }
-        return null;
+        var unknownComponent = angular.copy(types.unknownNodeComponent);
+        unknownComponent.clazz = clazz;
+        unknownComponent.configurationDescriptor.nodeDefinition.details = "Unknown Rule Node class: " + clazz;
+        return unknownComponent;
     }
 
     function resolveTargetRuleChains(ruleChainConnections) {
diff --git a/ui/src/app/api/time.service.js b/ui/src/app/api/time.service.js
index 8742b58..b8d0c41 100644
--- a/ui/src/app/api/time.service.js
+++ b/ui/src/app/api/time.service.js
@@ -32,84 +32,7 @@ const MAX_LIMIT = 500;
 /*@ngInject*/
 function TimeService($translate, types) {
 
-    var predefIntervals = [
-        {
-            name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'),
-            value: 1 * SECOND
-        },
-        {
-            name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'),
-            value: 5 * SECOND
-        },
-        {
-            name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
-            value: 10 * SECOND
-        },
-        {
-            name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'),
-            value: 15 * SECOND
-        },
-        {
-            name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
-            value: 30 * SECOND
-        },
-        {
-            name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
-            value: 1 * MINUTE
-        },
-        {
-            name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
-            value: 2 * MINUTE
-        },
-        {
-            name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
-            value: 5 * MINUTE
-        },
-        {
-            name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
-            value: 10 * MINUTE
-        },
-        {
-            name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'),
-            value: 15 * MINUTE
-        },
-        {
-            name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
-            value: 30 * MINUTE
-        },
-        {
-            name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
-            value: 1 * HOUR
-        },
-        {
-            name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
-            value: 2 * HOUR
-        },
-        {
-            name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'),
-            value: 5 * HOUR
-        },
-        {
-            name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
-            value: 10 * HOUR
-        },
-        {
-            name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'),
-            value: 12 * HOUR
-        },
-        {
-            name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
-            value: 1 * DAY
-        },
-        {
-            name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
-            value: 7 * DAY
-        },
-        {
-            name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
-            value: 30 * DAY
-        }
-    ];
+    var predefIntervals;
 
     var service = {
         minIntervalLimit: minIntervalLimit,
@@ -166,6 +89,7 @@ function TimeService($translate, types) {
         min = boundMinInterval(min);
         max = boundMaxInterval(max);
         var intervals = [];
+        initPredefIntervals();
         for (var i in predefIntervals) {
             var interval = predefIntervals[i];
             if (interval.value >= min && interval.value <= max) {
@@ -175,6 +99,89 @@ function TimeService($translate, types) {
         return intervals;
     }
 
+    function initPredefIntervals() {
+        if (!predefIntervals) {
+            predefIntervals = [
+                {
+                    name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'),
+                    value: 1 * SECOND
+                },
+                {
+                    name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'),
+                    value: 5 * SECOND
+                },
+                {
+                    name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
+                    value: 10 * SECOND
+                },
+                {
+                    name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'),
+                    value: 15 * SECOND
+                },
+                {
+                    name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
+                    value: 30 * SECOND
+                },
+                {
+                    name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
+                    value: 1 * MINUTE
+                },
+                {
+                    name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
+                    value: 2 * MINUTE
+                },
+                {
+                    name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
+                    value: 5 * MINUTE
+                },
+                {
+                    name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
+                    value: 10 * MINUTE
+                },
+                {
+                    name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'),
+                    value: 15 * MINUTE
+                },
+                {
+                    name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
+                    value: 30 * MINUTE
+                },
+                {
+                    name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
+                    value: 1 * HOUR
+                },
+                {
+                    name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
+                    value: 2 * HOUR
+                },
+                {
+                    name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'),
+                    value: 5 * HOUR
+                },
+                {
+                    name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
+                    value: 10 * HOUR
+                },
+                {
+                    name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'),
+                    value: 12 * HOUR
+                },
+                {
+                    name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
+                    value: 1 * DAY
+                },
+                {
+                    name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
+                    value: 7 * DAY
+                },
+                {
+                    name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
+                    value: 30 * DAY
+                }
+            ];
+        }
+    }
+
     function matchesExistingInterval(min, max, intervalMs) {
         var intervals = getIntervals(min, max);
         for (var i in intervals) {
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index cb85709..48c811b 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -488,7 +488,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
                         } else {
                             return true;
                         }
-                    } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
+                    } else if ((to.name === 'home.dashboards.dashboard' || to.name === 'dashboard')
+                        && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
                         return false;
                     } else {
                         return true;
@@ -504,10 +505,10 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
             var place = 'home.links';
             if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {
                 if (userHasDefaultDashboard()) {
-                    place = 'home.dashboards.dashboard';
+                    place = $rootScope.forceFullscreen ? 'dashboard' : 'home.dashboards.dashboard';
                     params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
                 } else if (isPublic()) {
-                    place = 'home.dashboards.dashboard';
+                    place = 'dashboard';
                     params = {dashboardId: lastPublicDashboardId};
                 }
             } else if (currentUser.authority === 'SYS_ADMIN') {
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
index a84bdde..edc2c2a 100644
--- a/ui/src/app/app.config.js
+++ b/ui/src/app/app.config.js
@@ -15,10 +15,6 @@
  */
 import injectTapEventPlugin from 'react-tap-event-plugin';
 import UrlHandler from './url.handler';
-import addLocaleKorean from './locale/locale.constant-ko';
-import addLocaleChinese from './locale/locale.constant-zh';
-import addLocaleRussian from './locale/locale.constant-ru';
-import addLocaleSpanish from './locale/locale.constant-es';
 
 /* eslint-disable import/no-unresolved, import/default */
 
@@ -38,46 +34,28 @@ export default function AppConfig($provide,
                                   $mdThemingProvider,
                                   $httpProvider,
                                   $translateProvider,
-                                  storeProvider,
-                                  locales) {
+                                  storeProvider) {
 
     injectTapEventPlugin();
     $locationProvider.html5Mode(true);
     $urlRouterProvider.otherwise(UrlHandler);
     storeProvider.setCaching(false);
-
-    $translateProvider.useSanitizeValueStrategy(null);
-    $translateProvider.useMissingTranslationHandler('tbMissingTranslationHandler');
-    $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
-    $translateProvider.fallbackLanguage('en_US');
-
-    addLocaleKorean(locales);
-    addLocaleChinese(locales);
-    addLocaleRussian(locales);
-    addLocaleSpanish(locales);
-
-    for (var langKey in locales) {
-        var translationTable = locales[langKey];
-        $translateProvider.translations(langKey, translationTable);
-    }
-
-    var lang = $translateProvider.resolveClientLocale();
-    if (lang) {
-        lang = lang.toLowerCase();
-        if (lang.startsWith('ko')) {
-            $translateProvider.preferredLanguage('ko_KR');
-        } else if (lang.startsWith('zh')) {
-            $translateProvider.preferredLanguage('zh_CN');
-        } else if (lang.startsWith('es')) {
-            $translateProvider.preferredLanguage('es_ES');
-        } else if (lang.startsWith('ru')) {
-            $translateProvider.preferredLanguage('ru_RU');
-        } else {
-            $translateProvider.preferredLanguage('en_US');
-        }
-    } else {
-        $translateProvider.preferredLanguage('en_US');
-    }
+    
+    $translateProvider.useSanitizeValueStrategy(null)
+                      .useMissingTranslationHandler('tbMissingTranslationHandler')
+                      .addInterpolation('$translateMessageFormatInterpolation')
+                      .useStaticFilesLoader({
+                          files: [
+                              {
+                                  prefix: PUBLIC_PATH + 'locale/locale.constant-', //eslint-disable-line
+                                  suffix: '.json'
+                              }
+                          ]
+                      })
+                      .registerAvailableLanguageKeys(SUPPORTED_LANGS, getLanguageAliases(SUPPORTED_LANGS)) //eslint-disable-line
+                      .fallbackLanguage('en_US') // must be before determinePreferredLanguage   
+                      .uniformLanguageTag('java')  // must be before determinePreferredLanguage
+                      .determinePreferredLanguage();                
 
     $httpProvider.interceptors.push('globalInterceptor');
 
@@ -168,4 +146,24 @@ export default function AppConfig($provide,
         //$mdThemingProvider.alwaysWatchTheme(true);
     }
 
+    function getLanguageAliases(supportedLangs) {
+        var aliases = {};
+
+        supportedLangs.sort().forEach(function(item, index, array) {
+            if (item.length === 2) { 
+                aliases[item] = item;
+                aliases[item + '_*'] = item;
+            } else {
+                var key = item.slice(0, 2);
+                if (index === 0 || key !== array[index - 1].slice(0, 2)) {
+                    aliases[key] = item;
+                    aliases[key + '_*'] = item;
+                } else {
+                    aliases[item] = item;
+                }
+            }
+        });
+        
+        return aliases;
+    }
 }
\ No newline at end of file
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index f021efb..c8cdeb0 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -51,7 +51,7 @@ import react from 'ngreact';
 import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
 import 'ngFlowchart/dist/ngFlowchart';
 
-import thingsboardLocales from './locale/locale.constant';
+import thingsboardTranslateHandler from './locale/translate-handler';
 import thingsboardLogin from './login';
 import thingsboardDialogs from './components/datakey-config-dialog.controller';
 import thingsboardMenu from './services/menu.service';
@@ -117,7 +117,7 @@ angular.module('thingsboard', [
     react.name,
     'flow',
     'flowchart',
-    thingsboardLocales,
+    thingsboardTranslateHandler,
     thingsboardLogin,
     thingsboardDialogs,
     thingsboardMenu,
diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js
index f3886b8..4667b09 100644
--- a/ui/src/app/app.run.js
+++ b/ui/src/app/app.run.js
@@ -113,7 +113,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, 
                             showForbiddenDialog();
                         } else if (to.redirectTo) {
                             evt.preventDefault();
-                            $state.go(to.redirectTo, params)
+                            $state.go(to.redirectTo, params);
+                        } else if (to.name === 'home.dashboards.dashboard' && $rootScope.forceFullscreen) {
+                            evt.preventDefault();
+                            $state.go('dashboard', params);
                         }
                     }
                 } else {
@@ -138,7 +141,7 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, 
         $rootScope.pageTitle = 'ThingsBoard';
 
         $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) {
-            if (userService.isPublic() && to.name === 'home.dashboards.dashboard') {
+            if (userService.isPublic() && to.name === 'dashboard') {
                 $location.search('publicId', userService.getPublicId());
                 userService.updateLastPublicDashboardId(params.dashboardId);
             }
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index cf1023e..1e34577 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -195,6 +195,21 @@ export default angular.module('thingsboard.types', [])
                 },
                 "ATTRIBUTES_READ": {
                     name: "audit-log.type-attributes-read"
+                },
+                "RELATION_ADD_OR_UPDATE": {
+                    name: "audit-log.type-relation-add-or-update"
+                },
+                "RELATION_DELETED": {
+                    name: "audit-log.type-relation-delete"
+                },
+                "RELATIONS_DELETED": {
+                    name: "audit-log.type-relations-delete"
+                },
+                "ALARM_ACK": {
+                    name: "audit-log.type-alarm-ack"
+                },
+                "ALARM_CLEAR": {
+                    name: "audit-log.type-alarm-clear"
                 }
             },
             auditLogActionStatus: {
@@ -366,6 +381,12 @@ export default angular.module('thingsboard.types', [])
                     list: 'entity.list-of-rulechains',
                     nameStartsWith: 'entity.rulechain-name-starts-with'
                 },
+                "RULE_NODE": {
+                    type: 'entity.type-rulenode',
+                    typePlural: 'entity.type-rulenodes',
+                    list: 'entity.list-of-rulenodes',
+                    nameStartsWith: 'entity.rulenode-name-starts-with'
+                },
                 "CURRENT_CUSTOMER": {
                     type: 'entity.type-current-customer',
                     list: 'entity.type-current-customer'
@@ -510,6 +531,22 @@ export default angular.module('thingsboard.types', [])
                     }
                 }
             },
+            unknownNodeComponent: {
+                type: 'UNKNOWN',
+                name: 'unknown',
+                clazz: 'tb.internal.Unknown',
+                configurationDescriptor: {
+                    nodeDefinition: {
+                        description: "",
+                        details: "",
+                        inEnabled: true,
+                        outEnabled: true,
+                        relationTypes: [],
+                        customRelations: false,
+                        defaultConfiguration: {}
+                    }
+                }
+            },
             inputNodeComponent: {
                 type: 'INPUT',
                 name: 'Input',
@@ -565,6 +602,75 @@ export default angular.module('thingsboard.types', [])
                     nodeClass: "tb-input-type",
                     icon: "input",
                     special: true
+                },
+                UNKNOWN: {
+                    value: "UNKNOWN",
+                    name: "rulenode.type-unknown",
+                    details: "rulenode.type-unknown-details",
+                    nodeClass: "tb-unknown-type",
+                    icon: "help_outline"
+                }
+            },
+            messageType: {
+                'POST_ATTRIBUTES_REQUEST': {
+                    name: 'Post attributes',
+                    value: 'POST_ATTRIBUTES_REQUEST'
+                },
+                'POST_TELEMETRY_REQUEST': {
+                    name: 'Post telemetry',
+                    value: 'POST_TELEMETRY_REQUEST'
+                },
+                'TO_SERVER_RPC_REQUEST': {
+                    name: 'RPC Request from Device',
+                    value: 'TO_SERVER_RPC_REQUEST'
+                },
+                'RPC_CALL_FROM_SERVER_TO_DEVICE': {
+                    name: 'RPC Request to Device',
+                    value: 'RPC_CALL_FROM_SERVER_TO_DEVICE'
+                },
+                'ACTIVITY_EVENT': {
+                    name: 'Activity Event',
+                    value: 'ACTIVITY_EVENT'
+                },
+                'INACTIVITY_EVENT': {
+                    name: 'Inactivity Event',
+                    value: 'INACTIVITY_EVENT'
+                },
+                'CONNECT_EVENT': {
+                    name: 'Connect Event',
+                    value: 'CONNECT_EVENT'
+                },
+                'DISCONNECT_EVENT': {
+                    name: 'Disconnect Event',
+                    value: 'DISCONNECT_EVENT'
+                },
+                'ENTITY_CREATED': {
+                    name: 'Entity Created',
+                    value: 'ENTITY_CREATED'
+                },
+                'ENTITY_UPDATED': {
+                    name: 'Entity Updated',
+                    value: 'ENTITY_UPDATED'
+                },
+                'ENTITY_DELETED': {
+                    name: 'Entity Deleted',
+                    value: 'ENTITY_DELETED'
+                },
+                'ENTITY_ASSIGNED': {
+                    name: 'Entity Assigned',
+                    value: 'ENTITY_ASSIGNED'
+                },
+                'ENTITY_UNASSIGNED': {
+                    name: 'Entity Unassigned',
+                    value: 'ENTITY_UNASSIGNED'
+                },
+                'ATTRIBUTES_UPDATED': {
+                    name: 'Attributes Updated',
+                    value: 'ATTRIBUTES_UPDATED'
+                },
+                'ATTRIBUTES_DELETED': {
+                    name: 'Attributes Deleted',
+                    value: 'ATTRIBUTES_DELETED'
                 }
             },
             valueType: {
diff --git a/ui/src/app/components/dashboard.scss b/ui/src/app/components/dashboard.scss
index 9f76347..ca99fcc 100644
--- a/ui/src/app/components/dashboard.scss
+++ b/ui/src/app/components/dashboard.scss
@@ -40,12 +40,12 @@ div.tb-widget {
     position: absolute;
     top: 8px;
     right: 8px;
-    z-index: 1;
-    margin: 0px;
+    z-index: 19;
+    margin: 0;
 
     .md-button.md-icon-button {
-      margin: 0px !important;
-      padding: 0px !important;
+      margin: 0 !important;
+      padding: 0 !important;
       line-height: 20px;
       width: 32px;
       height: 32px;
diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html
index 484164f..db6fd3b 100644
--- a/ui/src/app/components/datasource-entity.tpl.html
+++ b/ui/src/app/components/datasource-entity.tpl.html
@@ -60,7 +60,7 @@
 							<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
 								<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
 							</div>
-							<div layout="row" flex>
+							<div layout="row">
 							  <div class="tb-chip-label">
 							  	{{$chip.label}}
 							  </div>
@@ -112,7 +112,7 @@
 							  <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
 								  <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
 							  </div>
-							  <div layout="row" flex>
+							  <div layout="row">
 								  <div class="tb-chip-label">
 									  {{$chip.label}}
 								  </div>
@@ -164,7 +164,7 @@
 						   <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
 							   <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
 						   </div>
-						   <div layout="row" flex>
+						   <div layout="row">
 							   <div class="tb-chip-label">
 								   {{$chip.label}}
 							   </div>
diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html
index 134dcd7..6bf49ba 100644
--- a/ui/src/app/components/datasource-func.tpl.html
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -61,7 +61,7 @@
 						  <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
 							  <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
 						  </div>
-						  <div layout="row" flex>
+						  <div layout="row">
 							  <div class="tb-chip-label">
 								  {{$chip.label}}
 							  </div>
@@ -112,7 +112,7 @@
 					<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
 						<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
 					</div>
-					<div layout="row" flex>
+					<div layout="row">
 						<div class="tb-chip-label">
 							{{$chip.label}}
 						</div>
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index 360b133..c7e9919 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -59,14 +59,4 @@ md-sidenav.tb-sidenav-details {
       background-color: $primary-hue-3;
     }
   }
-
-  md-tab-content.md-active > div {
-    height: 100%;
-    & > *:first-child {
-      height: 100%;
-    }
-    md-content {
-      height: 100%;
-    }
-  }
 }
diff --git a/ui/src/app/components/json-content.directive.js b/ui/src/app/components/json-content.directive.js
index e945079..1027486 100644
--- a/ui/src/app/components/json-content.directive.js
+++ b/ui/src/app/components/json-content.directive.js
@@ -18,8 +18,8 @@ import './json-content.scss';
 import 'brace/ext/language_tools';
 import 'brace/mode/json';
 import 'brace/mode/text';
-import 'ace-builds/src-min-noconflict/snippets/json';
-import 'ace-builds/src-min-noconflict/snippets/text';
+import 'brace/snippets/json';
+import 'brace/snippets/text';
 
 import fixAceEditor from './ace-editor-fix';
 
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js
index 215b7b9..9364689 100644
--- a/ui/src/app/components/json-object-edit.directive.js
+++ b/ui/src/app/components/json-object-edit.directive.js
@@ -17,7 +17,7 @@ import './json-object-edit.scss';
 
 import 'brace/ext/language_tools';
 import 'brace/mode/json';
-import 'ace-builds/src-min-noconflict/snippets/json';
+import 'brace/snippets/json';
 
 import fixAceEditor from './ace-editor-fix';
 
diff --git a/ui/src/app/components/widget/action/manage-widget-actions.directive.js b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
index 81c40cd..88a37a8 100644
--- a/ui/src/app/components/widget/action/manage-widget-actions.directive.js
+++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js
@@ -111,8 +111,15 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
         }
     });
 
-    function enterFilterMode () {
+    function enterFilterMode (event) {
+        let $button = angular.element(event.currentTarget);
+        let $toolbarsContainer = $button.closest('.toolbarsContainer');
+
         vm.query.search = '';
+
+        $timeout(()=>{
+            $toolbarsContainer.find('.searchInput').focus();
+        })
     }
 
     function exitFilterMode () {
diff --git a/ui/src/app/components/widget/action/manage-widget-actions.tpl.html b/ui/src/app/components/widget/action/manage-widget-actions.tpl.html
index fc9262e..07f76f2 100644
--- a/ui/src/app/components/widget/action/manage-widget-actions.tpl.html
+++ b/ui/src/app/components/widget/action/manage-widget-actions.tpl.html
@@ -15,7 +15,7 @@
     limitations under the License.
 
 -->
-<div ng-form="manageWidgetActionsForm" class="tb-manage-widget-actions md-whiteframe-z1" layout="column">
+<div ng-form="manageWidgetActionsForm" class="tb-manage-widget-actions md-whiteframe-z1 toolbarsContainer" layout="column">
     <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search === null">
         <div class="md-toolbar-tools">
             <span translate>widget-config.actions</span>
@@ -26,7 +26,7 @@
                     {{ 'widget-config.add-action' | translate }}
                 </md-tooltip>
             </md-button>
-            <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+            <md-button class="md-icon-button" ng-click="vm.enterFilterMode($event)">
                 <md-icon>search</md-icon>
                 <md-tooltip md-direction="top">
                     {{ 'action.search' | translate }}
@@ -44,7 +44,7 @@
             </md-button>
             <md-input-container flex>
                 <label>&nbsp;</label>
-                <input ng-model="vm.query.search" name="querySearchInput" placeholder="{{ 'widget-config.search-actions' | translate }}"/>
+                <input ng-model="vm.query.search" class="searchInput" name="querySearchInput" placeholder="{{ 'widget-config.search-actions' | translate }}"/>
             </md-input-container>
             <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
                 <md-icon aria-label="Close" class="material-icons">close</md-icon>
diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js
index 9feb40d..36e5bee 100644
--- a/ui/src/app/components/widget/widget.controller.js
+++ b/ui/src/app/components/widget/widget.controller.js
@@ -479,7 +479,11 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
                     dashboardId: targetDashboardId,
                     state: utils.objToBase64([ stateObject ])
                 }
-                $state.go('home.dashboards.dashboard', stateParams);
+                if ($state.current.name === 'dashboard') {
+                    $state.go('dashboard', stateParams);
+                } else {
+                    $state.go('home.dashboards.dashboard', stateParams);
+                }
                 break;
             case types.widgetActionTypes.custom.value:
                 var customFunction = descriptor.customFunction;
diff --git a/ui/src/app/components/widget/widget-config.tpl.html b/ui/src/app/components/widget/widget-config.tpl.html
index ce796d4..e8762bd 100644
--- a/ui/src/app/components/widget/widget-config.tpl.html
+++ b/ui/src/app/components/widget/widget-config.tpl.html
@@ -187,17 +187,17 @@
                 </div>
                 <div layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
                     <div layout="row" layout-padding>
-                        <md-checkbox flex aria-label="{{ 'widget-config.display-title' | translate }}"
+                        <md-checkbox aria-label="{{ 'widget-config.display-title' | translate }}"
                                      ng-model="showTitle">{{ 'widget-config.display-title' | translate }}
                         </md-checkbox>
                     </div>
                     <div layout="row" layout-padding>
-                        <md-checkbox flex aria-label="{{ 'widget-config.drop-shadow' | translate }}"
+                        <md-checkbox aria-label="{{ 'widget-config.drop-shadow' | translate }}"
                                      ng-model="dropShadow">{{ 'widget-config.drop-shadow' | translate }}
                         </md-checkbox>
                     </div>
                     <div layout="row" layout-padding>
-                        <md-checkbox flex aria-label="{{ 'widget-config.enable-fullscreen' | translate }}"
+                        <md-checkbox aria-label="{{ 'widget-config.enable-fullscreen' | translate }}"
                                      ng-model="enableFullscreen">{{ 'widget-config.enable-fullscreen' | translate }}
                         </md-checkbox>
                     </div>
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index f672f3f..6df48df 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -196,6 +196,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
     vm.displayDashboardTimewindow = displayDashboardTimewindow;
     vm.displayDashboardsSelect = displayDashboardsSelect;
     vm.displayEntitiesSelect = displayEntitiesSelect;
+    vm.hideFullscreenButton = hideFullscreenButton;
 
     vm.widgetsBundle;
 
@@ -258,7 +259,11 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                     dashboardId: vm.currentDashboardId
                 });
             } else {
-                $state.go('home.dashboards.dashboard', {dashboardId: vm.currentDashboardId});
+                if ($state.current.name === 'dashboard') {
+                    $state.go('dashboard', {dashboardId: vm.currentDashboardId});
+                } else {
+                    $state.go('home.dashboards.dashboard', {dashboardId: vm.currentDashboardId});
+                }
             }
         }
     });
@@ -805,6 +810,10 @@ export default function DashboardController(types, utils, dashboardUtils, widget
         }
     }
 
+    function hideFullscreenButton() {
+        return vm.widgetEditMode || vm.iframeMode || $rootScope.forceFullscreen || $state.current.name === 'dashboard';
+    }
+
     function onRevertWidgetEdit(widgetForm) {
         if (widgetForm.$dirty) {
             widgetForm.$setPristine();
diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js
index ccb43c7..4572ac3 100644
--- a/ui/src/app/dashboard/dashboard.routes.js
+++ b/ui/src/app/dashboard/dashboard.routes.js
@@ -86,6 +86,24 @@ export default function DashboardRoutes($stateProvider) {
                 label: '{"icon": "dashboard", "label": "{{ vm.dashboard.title }}", "translate": "false"}'
             }
         })
+        .state('dashboard', {
+            url: '/dashboard/:dashboardId?state',
+            reloadOnSearch: false,
+            module: 'private',
+            auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
+            views: {
+                "@": {
+                    templateUrl: dashboardTemplate,
+                    controller: 'DashboardController',
+                    controllerAs: 'vm'
+                }
+            },
+            data: {
+                widgetEditMode: false,
+                searchEnabled: false,
+                pageTitle: 'dashboard.dashboard'
+            }
+        })
         .state('home.customers.dashboards.dashboard', {
             url: '/:dashboardId?state',
             reloadOnSearch: false,
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 9626509..829f174 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-content style="padding-top: 150px;" flex tb-expand-fullscreen="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-button-id="dashboard-expand-button"
-            hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom" ng-if="vm.dashboard">
+            hide-expand-button="vm.hideFullscreenButton()" expand-tooltip-direction="bottom" ng-if="vm.dashboard">
     <section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
              ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
         <tb-dashboard-toolbar ng-show="!vm.widgetEditMode" force-fullscreen="forceFullscreen"
diff --git a/ui/src/app/dashboard/states/select-target-state.tpl.html b/ui/src/app/dashboard/states/select-target-state.tpl.html
index 5fd3122..a9a4348 100644
--- a/ui/src/app/dashboard/states/select-target-state.tpl.html
+++ b/ui/src/app/dashboard/states/select-target-state.tpl.html
@@ -41,8 +41,8 @@
         </md-dialog-content>
         <md-dialog-actions layout="row">
             <span flex></span>
-            <md-button ng-disabled="$root.loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
-                {{ 'action.save' | translate }}
+            <md-button ng-disabled="$root.loading || !theForm.$valid" type="submit" class="md-raised md-primary">
+                {{ 'action.select' | translate }}
             </md-button>
             <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
         </md-dialog-actions>
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index 522f1ab..fbda549 100644
--- a/ui/src/app/device/device-card.tpl.html
+++ b/ui/src/app/device/device-card.tpl.html
@@ -16,7 +16,8 @@
 
 -->
 <div flex layout="column" style="margin-top: -10px;">
-    <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
-    <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
-    <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
+    <div style="text-transform: uppercase; padding-bottom: 5px;">{{vm.item.type}}</div>
+    <div class="tb-card-description">{{vm.item.additionalInfo.description}}</div>
+    <div style="padding-top: 5px;" class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+    <div style="padding-top: 5px;" class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
 </div>
diff --git a/ui/src/app/entity/alias/entity-aliases.tpl.html b/ui/src/app/entity/alias/entity-aliases.tpl.html
index a760eab..8eb7ff5 100644
--- a/ui/src/app/entity/alias/entity-aliases.tpl.html
+++ b/ui/src/app/entity/alias/entity-aliases.tpl.html
@@ -43,7 +43,7 @@
 				<fieldset ng-disabled="$root.loading">
 					<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="entityAlias in vm.entityAliases track by $index">
 						<span flex="5">{{$index + 1}}.</span>
-						<di class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
+						<div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
 							<md-input-container flex="20" style="min-width: 150px;" md-no-float class="md-block">
 								<input required name="alias" placeholder="{{ 'entity.alias' | translate }}" ng-model="entityAlias.alias">
 								<div ng-messages="aliasForm.alias.$error">
@@ -81,7 +81,7 @@
 									close
 								</md-icon>
 							</md-button>
-						</di>
+						</div>
 					</div>
 				</fieldset>
 			</div>
diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js
index b551ae6..0061854 100644
--- a/ui/src/app/entity/attribute/attribute-table.directive.js
+++ b/ui/src/app/entity/attribute/attribute-table.directive.js
@@ -30,7 +30,7 @@ import AliasController from '../../api/alias-controller';
 
 /*@ngInject*/
 export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
-                                                $mdUtil, $document, $translate, $filter, utils, types, dashboardUtils,
+                                                $mdUtil, $document, $translate, $filter, $timeout, utils, types, dashboardUtils,
                                                 entityService, attributeService, widgetService) {
 
     var linker = function (scope, element, attrs) {
@@ -110,8 +110,15 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
             scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
         }
 
-        scope.enterFilterMode = function() {
+        scope.enterFilterMode = function(event) {
+            let $button = angular.element(event.currentTarget);
+            let $toolbarsContainer = $button.closest('.toolbarsContainer');
+
             scope.query.search = '';
+
+            $timeout(()=>{
+                $toolbarsContainer.find('.searchInput').focus();
+            })
         }
 
         scope.exitFilterMode = function() {
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
index d55a6da..ecbcf00 100644
--- a/ui/src/app/entity/attribute/attribute-table.tpl.html
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -26,7 +26,7 @@
             </md-select>
         </md-input-container>
     </section>
-    <div class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
+    <div class="md-whiteframe-z1 toolbarsContainer" ng-class="{flex: mode==='widget'}">
         <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
                                                                  && !selectedAttributes.length
                                                                  && query.search === null">
@@ -39,7 +39,7 @@
                         {{ 'action.add' | translate }}
                     </md-tooltip>
                 </md-button>
-                <md-button class="md-icon-button" ng-click="enterFilterMode()">
+                <md-button class="md-icon-button" ng-click="enterFilterMode($event)">
                     <md-icon>search</md-icon>
                     <md-tooltip md-direction="top">
                         {{ 'action.search' | translate }}
@@ -65,7 +65,7 @@
                 </md-button>
                 <md-input-container flex>
                     <label>&nbsp;</label>
-                    <input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+                    <input ng-model="query.search" class="searchInput" placeholder="{{ 'common.enter-search' | translate }}"/>
                 </md-input-container>
                 <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitFilterMode()">
                     <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
diff --git a/ui/src/app/entity/entity-select.directive.js b/ui/src/app/entity/entity-select.directive.js
index 8e4031c..e7ac5fd 100644
--- a/ui/src/app/entity/entity-select.directive.js
+++ b/ui/src/app/entity/entity-select.directive.js
@@ -22,14 +22,28 @@ import entitySelectTemplate from './entity-select.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function EntitySelect($compile, $templateCache) {
+export default function EntitySelect($compile, $templateCache, entityService) {
 
     var linker = function (scope, element, attrs, ngModelCtrl) {
         var template = $templateCache.get(entitySelectTemplate);
         element.html(template);
 
         scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
-        scope.model = {};
+
+        var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes, scope.useAliasEntityTypes);
+
+        var entityTypeKeys = Object.keys(entityTypes);
+
+        if (entityTypeKeys.length === 1) {
+            scope.displayEntityTypeSelect = false;
+            scope.defaultEntityType = entityTypes[entityTypeKeys[0]];
+        } else {
+            scope.displayEntityTypeSelect = true;
+        }
+
+        scope.model = {
+            entityType: scope.defaultEntityType
+        };
 
         scope.updateView = function () {
             if (!scope.disabled) {
@@ -54,7 +68,7 @@ export default function EntitySelect($compile, $templateCache) {
                 scope.model.entityType = value.entityType;
                 scope.model.entityId = value.id;
             } else {
-                scope.model.entityType = null;
+                scope.model.entityType = scope.defaultEntityType;
                 scope.model.entityId = null;
             }
             initWatchers();
@@ -106,6 +120,7 @@ export default function EntitySelect($compile, $templateCache) {
             theForm: '=?',
             tbRequired: '=?',
             disabled:'=ngDisabled',
+            allowedEntityTypes: "=?",
             useAliasEntityTypes: "=?"
         }
     };
diff --git a/ui/src/app/entity/entity-select.tpl.html b/ui/src/app/entity/entity-select.tpl.html
index 9e5e227..d6b6eea 100644
--- a/ui/src/app/entity/entity-select.tpl.html
+++ b/ui/src/app/entity/entity-select.tpl.html
@@ -17,10 +17,12 @@
 -->
 <div layout='row' class="tb-entity-select">
     <tb-entity-type-select style="min-width: 100px;"
+                           ng-if="displayEntityTypeSelect"
                            the-form="theForm"
                            ng-disabled="disabled"
                            tb-required="tbRequired"
                            use-alias-entity-types="useAliasEntityTypes"
+                           allowed-entity-types="allowedEntityTypes"
                            ng-model="model.entityType">
     </tb-entity-type-select>
     <tb-entity-autocomplete flex ng-if="model.entityType"
diff --git a/ui/src/app/entity/entity-type-list.directive.js b/ui/src/app/entity/entity-type-list.directive.js
index f042e5c..8a680c5 100644
--- a/ui/src/app/entity/entity-type-list.directive.js
+++ b/ui/src/app/entity/entity-type-list.directive.js
@@ -35,7 +35,30 @@ export default function EntityTypeListDirective($compile, $templateCache, $q, $m
                                 : $translate.instant('entity.any-entity');
         scope.secondaryPlaceholder = '+' + $translate.instant('entity.entity-type');
 
-        var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
+        var entityTypes;
+
+        if (scope.ignoreAuthorityFilter && scope.allowedEntityTypes
+            && scope.allowedEntityTypes.length) {
+            entityTypes = {};
+            scope.allowedEntityTypes.forEach((entityTypeValue) => {
+                var entityType = entityTypeFromValue(entityTypeValue);
+                if (entityType) {
+                    entityTypes[entityType] = entityTypeValue;
+                }
+            });
+        } else {
+            entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
+        }
+
+        function entityTypeFromValue(entityTypeValue) {
+            for (var entityType in types.entityType) {
+                if (types.entityType[entityType] === entityTypeValue) {
+                    return entityType;
+                }
+            }
+            return null;
+        }
+
         scope.entityTypesList = [];
         for (var type in entityTypes) {
             var entityTypeInfo = {};
@@ -62,28 +85,43 @@ export default function EntityTypeListDirective($compile, $templateCache, $q, $m
         }
 
         ngModelCtrl.$render = function () {
-            scope.entityTypeList = [];
+            if (scope.entityTypeListWatch) {
+                scope.entityTypeListWatch();
+                scope.entityTypeListWatch = null;
+            }
+            var entityTypeList = [];
             var value = ngModelCtrl.$viewValue;
             if (value && value.length) {
                 value.forEach(function(type) {
                     var entityTypeInfo = {};
                     entityTypeInfo.value = type;
                     entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + '';
-                    scope.entityTypeList.push(entityTypeInfo);
+                    entityTypeList.push(entityTypeInfo);
                 });
             }
+            scope.entityTypeList = entityTypeList;
+            scope.entityTypeListWatch = scope.$watch('entityTypeList', function (newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    updateEntityTypeList();
+                }
+            }, true);
         }
 
-        scope.$watch('entityTypeList', function () {
-            var values = [];
+        function updateEntityTypeList() {
+            var values = ngModelCtrl.$viewValue;
+            if (!values) {
+                values = [];
+                ngModelCtrl.$setViewValue(values);
+            } else {
+                values.length = 0;
+            }
             if (scope.entityTypeList && scope.entityTypeList.length) {
-                scope.entityTypeList.forEach(function(entityType) {
+                scope.entityTypeList.forEach(function (entityType) {
                     values.push(entityType.value);
                 });
             }
-            ngModelCtrl.$setViewValue(values);
             scope.updateValidity();
-        }, true);
+        }
 
         $compile(element.contents())(scope);
 
@@ -103,7 +141,8 @@ export default function EntityTypeListDirective($compile, $templateCache, $q, $m
         scope: {
             disabled:'=ngDisabled',
             tbRequired: '=?',
-            allowedEntityTypes: '=?'
+            allowedEntityTypes: '=?',
+            ignoreAuthorityFilter: '=?'
         }
     };
 
diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js
index 9ab66ca..e945fb6 100644
--- a/ui/src/app/entity/relation/relation-filters.directive.js
+++ b/ui/src/app/entity/relation/relation-filters.directive.js
@@ -44,6 +44,10 @@ export default function RelationFilters($compile, $templateCache) {
         scope.removeFilter = removeFilter;
 
         ngModelCtrl.$render = function () {
+            if (scope.relationFiltersWatch) {
+                scope.relationFiltersWatch();
+                scope.relationFiltersWatch = null;
+            }
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
                 scope.relationFilters.length = 0;
@@ -51,7 +55,7 @@ export default function RelationFilters($compile, $templateCache) {
                     scope.relationFilters.push(filter);
                 });
             }
-            scope.$watch('relationFilters', function (newVal, prevVal) {
+            scope.relationFiltersWatch = scope.$watch('relationFilters', function (newVal, prevVal) {
                 if (!angular.equals(newVal, prevVal)) {
                     updateValue();
                 }
@@ -74,11 +78,16 @@ export default function RelationFilters($compile, $templateCache) {
         }
 
         function updateValue() {
-            var value = [];
+            var value = ngModelCtrl.$viewValue;
+            if (!value) {
+                value = [];
+                ngModelCtrl.$setViewValue(value);
+            } else {
+                value.length = 0;
+            }
             scope.relationFilters.forEach(function (filter) {
                 value.push(filter);
             });
-            ngModelCtrl.$setViewValue(value);
         }
         $compile(element.contents())(scope);
     }
diff --git a/ui/src/app/entity/relation/relation-table.directive.js b/ui/src/app/entity/relation/relation-table.directive.js
index 3247811..872042c 100644
--- a/ui/src/app/entity/relation/relation-table.directive.js
+++ b/ui/src/app/entity/relation/relation-table.directive.js
@@ -41,7 +41,7 @@ export default function RelationTable() {
 }
 
 /*@ngInject*/
-function RelationTableController($scope, $q, $mdDialog, $document, $translate, $filter, utils, types, entityRelationService) {
+function RelationTableController($scope, $q, $mdDialog, $document, $translate, $filter, $timeout, utils, types, entityRelationService) {
 
     let vm = this;
 
@@ -90,8 +90,15 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
         }
     });
 
-    function enterFilterMode () {
+    function enterFilterMode (event) {
+        let $button = angular.element(event.currentTarget);
+        let $toolbarsContainer = $button.closest('.toolbarsContainer');
+
         vm.query.search = '';
+
+        $timeout(()=>{
+            $toolbarsContainer.find('.searchInput').focus();
+        })
     }
 
     function exitFilterMode () {
diff --git a/ui/src/app/entity/relation/relation-table.tpl.html b/ui/src/app/entity/relation/relation-table.tpl.html
index 16d422c..a2b0920 100644
--- a/ui/src/app/entity/relation/relation-table.tpl.html
+++ b/ui/src/app/entity/relation/relation-table.tpl.html
@@ -26,7 +26,7 @@
             </md-select>
         </md-input-container>
     </section>
-    <div layout="column" class="md-whiteframe-z1">
+    <div layout="column" class="md-whiteframe-z1 toolbarsContainer">
         <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
                                                                  && vm.query.search === null">
             <div class="md-toolbar-tools">
@@ -39,7 +39,7 @@
                         {{ 'action.add' | translate }}
                     </md-tooltip>
                 </md-button>
-                <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+                <md-button class="md-icon-button" ng-click="vm.enterFilterMode($event)">
                     <md-icon>search</md-icon>
                     <md-tooltip md-direction="top">
                         {{ 'action.search' | translate }}
@@ -64,7 +64,7 @@
                 </md-button>
                 <md-input-container flex>
                     <label>&nbsp;</label>
-                    <input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+                    <input ng-model="vm.query.search" class="searchInput" placeholder="{{ 'common.enter-search' | translate }}"/>
                 </md-input-container>
                 <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
                     <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js
index ecd3865..18d281c 100644
--- a/ui/src/app/extension/extension-table.directive.js
+++ b/ui/src/app/extension/extension-table.directive.js
@@ -45,7 +45,7 @@ export default function ExtensionTableDirective() {
 }
 
 /*@ngInject*/
-function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
+function ExtensionTableController($scope, $filter, $document, $translate, $timeout, $mdDialog, types, attributeService, telemetryWebsocketService, importExport) {
 
     let vm = this;
 
@@ -141,11 +141,17 @@ function ExtensionTableController($scope, $filter, $document, $translate, types,
         }
     });
 
-    function enterFilterMode() {
+    function enterFilterMode(event) {
+        let $button = angular.element(event.currentTarget);
+        let $toolbarsContainer = $button.closest('.toolbarsContainer');
+
         vm.query.search = '';
         if(vm.inWidget) {
             vm.ctx.hideTitlePanel = true;
         }
+        $timeout(()=>{
+            $toolbarsContainer.find('.searchInput').focus();
+        })
     }
 
     function exitFilterMode() {
diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html
index 0ad2f4d..6a482b9 100644
--- a/ui/src/app/extension/extension-table.tpl.html
+++ b/ui/src/app/extension/extension-table.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-content flex class="md-padding tb-absolute-fill tb-data-table extension-table" layout="column">
-    <div layout="column" class="md-whiteframe-z1" ng-class="{'tb-absolute-fill' : vm.inWidget}">
+    <div layout="column" class="md-whiteframe-z1 toolbarsContainer" ng-class="{'tb-absolute-fill' : vm.inWidget}">
         <md-toolbar ng-if="!vm.inWidget" class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
                                                                                       && vm.query.search === null">
             <div class="md-toolbar-tools">
@@ -41,7 +41,7 @@
                         {{ 'action.add' | translate }}
                     </md-tooltip>
                 </md-button>
-                <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+                <md-button class="md-icon-button" ng-click="vm.enterFilterMode($event)">
                     <md-icon>search</md-icon>
                     <md-tooltip md-direction="top">
                         {{ 'action.search' | translate }}
@@ -66,7 +66,7 @@
                 </md-button>
                 <md-input-container flex>
                     <label>&nbsp;</label>
-                    <input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+                    <input ng-model="vm.query.search" class="searchInput" placeholder="{{ 'common.enter-search' | translate }}"/>
                 </md-input-container>
                 <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
                     <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js
index 1b708a9..458c118 100644
--- a/ui/src/app/help/help-links.constant.js
+++ b/ui/src/app/help/help-links.constant.js
@@ -20,6 +20,7 @@ var ruleNodeClazzHelpLinkMap = {
     'org.thingsboard.rule.engine.filter.TbJsSwitchNode': 'ruleNodeJsSwitch',
     'org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode': 'ruleNodeMessageTypeFilter',
     'org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode': 'ruleNodeMessageTypeSwitch',
+    'org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode': 'ruleNodeOriginatorTypeFilter',
     'org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode': 'ruleNodeOriginatorTypeSwitch',
     'org.thingsboard.rule.engine.metadata.TbGetAttributesNode': 'ruleNodeOriginatorAttributes',
     'org.thingsboard.rule.engine.metadata.TbGetOriginatorFieldsNode': 'ruleNodeOriginatorFields',
@@ -31,7 +32,8 @@ var ruleNodeClazzHelpLinkMap = {
     'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg',
     'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail',
     'org.thingsboard.rule.engine.action.TbClearAlarmNode': 'ruleNodeClearAlarm',
-    'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCrateAlarm',
+    'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCreateAlarm',
+    'org.thingsboard.rule.engine.delay.TbMsgDelayNode': 'ruleNodeMsgDelay',
     'org.thingsboard.rule.engine.debug.TbMsgGeneratorNode': 'ruleNodeMsgGenerator',
     'org.thingsboard.rule.engine.action.TbLogNode': 'ruleNodeLog',
     'org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode': 'ruleNodeRpcCallReply',
@@ -61,6 +63,7 @@ export default angular.module('thingsboard.help', [])
                 ruleNodeJsSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#switch-node",
                 ruleNodeMessageTypeFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node",
                 ruleNodeMessageTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node",
+                ruleNodeOriginatorTypeFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node",
                 ruleNodeOriginatorTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node",
                 ruleNodeOriginatorAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes",
                 ruleNodeOriginatorFields: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-fields",
@@ -72,7 +75,8 @@ export default angular.module('thingsboard.help', [])
                 ruleNodeTransformMsg: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node",
                 ruleNodeMsgToEmail: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node",
                 ruleNodeClearAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node",
-                ruleNodeCrateAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node",
+                ruleNodeCreateAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node",
+                ruleNodeMsgDelay: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#delay-node",
                 ruleNodeMsgGenerator: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#generator-node",
                 ruleNodeLog: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#log-node",
                 ruleNodeRpcCallReply: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node",
diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json
new file mode 100644
index 0000000..1454d36
--- /dev/null
+++ b/ui/src/app/locale/locale.constant-it_IT.json
@@ -0,0 +1,1445 @@
+{
+    "access": {
+        "unauthorized": "Non autorizzato",
+        "unauthorized-access": "Accesso non autorizzato",
+        "unauthorized-access-text": "Devi effettuare il login per accedere a questa risorsa!",
+        "access-forbidden": "Accesso Vietato",
+        "access-forbidden-text": "Non hai i diritti di accesso a questa posizione!<br/>Prova ad effettuare il login con un diverso account.",
+        "refresh-token-expired": "Sessione scaduta",
+        "refresh-token-failed": "Impossibile aggiornare la sessione"
+    },
+    "action": {
+        "activate": "Attiva",
+        "suspend": "Sospendi",
+        "save": "Salva",
+        "saveAs": "Salva come",
+        "cancel": "Cancella",
+        "ok": "OK",
+        "delete": "Elimina",
+        "add": "Aggiungi",
+        "yes": "Sì",
+        "no": "No",
+        "update": "Aggiorna",
+        "remove": "Rimuovi",
+        "search": "Cerca",
+        "clear-search": "Cancella ricerca",
+        "assign": "Assegna",
+        "unassign": "Annulla assegnazione",
+        "share": "Condividi",
+        "make-private": "Rendi privato",
+        "apply": "Applica",
+        "apply-changes": "Applica modifiche",
+        "edit-mode": "Modalità modifica",
+        "enter-edit-mode": "Attiva la modalità di modifica",
+        "decline-changes": "Annulla le modifiche",
+        "close": "Chiudi",
+        "back": "Indietro",
+        "run": "Esegui",
+        "sign-in": "Registrati!",
+        "edit": "Modifica",
+        "view": "Visualizza",
+        "create": "Crea",
+        "drag": "Trascina",
+        "refresh": "Aggiorna",
+        "undo": "Annulla",
+        "copy": "Copia",
+        "paste": "Incolla",
+        "copy-reference": "Copia riferimento",
+        "paste-reference": "Incolla riferimento",
+        "import": "Importa",
+        "export": "Esporta",
+        "share-via": "Condividi con {{provider}}"
+    },
+    "aggregation": {
+        "aggregation": "Aggregazione",
+        "function": "Funzione di aggregazione dei dati",
+        "limit": "Valori max",
+        "group-interval": "Intervallo di raggruppamento",
+        "min": "Min",
+        "max": "Max",
+        "avg": "Media",
+        "sum": "Somma",
+        "count": "Conteggio",
+        "none": "Nessuna"
+    },
+    "admin": {
+        "general": "Generale",
+        "general-settings": "Impostazioni Generali",
+        "outgoing-mail": "Posta in uscita",
+        "outgoing-mail-settings": "Impostazioni Posta in uscita",
+        "system-settings": "Impostazioni di sistema",
+        "test-mail-sent": "Mail di test inviata con successo!",
+        "base-url": "URL di base",
+        "base-url-required": "URL di base obbligatoria.",
+        "mail-from": "Mittente",
+        "mail-from-required": "Mittente obbligatorio.",
+        "smtp-protocol": "Protocollo SMTP",
+        "smtp-host": "Host SMTP",
+        "smtp-host-required": "Host SMTP obbligatorio.",
+        "smtp-port": "Porta SMTP",
+        "smtp-port-required": "Porta SMTP obbligatoria.",
+        "smtp-port-invalid": "Numero di porta SMTP non valido.",
+        "timeout-msec": "Timeout (msec)",
+        "timeout-required": "Timeout obbligatorio.",
+        "timeout-invalid": "Timeout non valido.",
+        "enable-tls": "Abilita TLS",
+        "send-test-mail": "Invia mail di test"
+    },
+    "alarm": {
+        "alarm": "Allarme",
+        "alarms": "Allarmi",
+        "select-alarm": "Seleziona un allarme",
+        "no-alarms-matching": "Nessun allarme corrispondente a '{{entity}}' è stato trovato.",
+        "alarm-required": "Allarme richiesto",
+        "alarm-status": "Stato Allarme",
+        "search-status": {
+            "ANY": "Qualsiasi",
+            "ACTIVE": "Attivo",
+            "CLEARED": "Cancellato",
+            "ACK": "Riconosciuto",
+            "UNACK": "Non riconosciuto"
+        },
+        "display-status": {
+            "ACTIVE_UNACK": "Active Unacknowledged", 
+            "ACTIVE_ACK": "Active Acknowledged",
+            "CLEARED_UNACK": "Cleared Unacknowledged",
+            "CLEARED_ACK": "Cleared Acknowledged"
+        },
+        "no-alarms-prompt": "Nessun allarme trovato",
+        "created-time": "Orario di creazione",
+        "type": "Tipo",
+        "severity": "Gravità",
+        "originator": "Origine", 
+        "originator-type": "Tipo origine", 
+        "details": "Dettagli",
+        "status": "Stato",
+        "alarm-details": "Dettagli allarme",
+        "start-time": "Orario inizio",
+        "end-time": "Orario fine",
+        "ack-time": "Orario conferma",
+        "clear-time": "Orario cancellazione",
+        "severity-critical": "Critico",
+        "severity-major": "Maggiore",
+        "severity-minor": "Minore",
+        "severity-warning": "Avviso",
+        "severity-indeterminate": "Indeterminato",
+        "acknowledge": "Conferma",
+        "clear": "Cancella",
+        "search": "Ricerca allarmi",
+        "selected-alarms": "{ count, plural, 1 {1 allarme selezionato} other {# allarmi selezionati} }",
+        "no-data": "Nessun dato da visualizzare",
+        "polling-interval": "Intervallo di polling (sec) Allarmi",
+        "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} }?", 
+        "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} }?" 
+    },
+    "alias": {
+        "add": "Aggiungi alias",
+        "edit": "Modifica alias",
+        "name": "Nome Alias",
+        "name-required": "Nome Alias obbligatorio",
+        "duplicate-alias": "Un Alias con lo stesso nome è già presente.",
+        "filter-type-single-entity": "Singola entità",
+        "filter-type-entity-list": "Lista Entità",
+        "filter-type-entity-name": "Nome Entità",
+        "filter-type-state-entity": "Entity from dashboard state",
+        "filter-type-state-entity-description": "Entità prelevata dai parametri di stato della dashboard",
+        "filter-type-asset-type": "Tipo di Asset",
+        "filter-type-asset-type-description": "Asset di tipo '{{assetType}}'",
+        "filter-type-asset-type-and-name-description": "Asset di tipo '{{assetType}}' e con un nome che inizia per '{{prefix}}'",
+        "filter-type-device-type": "Tipo di dispositivo",
+        "filter-type-device-type-description": "Dispositivi di tipo '{{deviceType}}'",
+        "filter-type-device-type-and-name-description": "Dispositivi di tipo '{{deviceType}}' e con un nome che inizia per '{{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": "Filtro entità",
+        "resolve-multiple": "Resolve as multiple entities",
+        "filter-type": "Tipo di filtro",
+        "filter-type-required": "Tipo di filtro richiesto.",
+        "entity-filter-no-entity-matched": "Nessuna entità corrispondente al filtro specificato è stata trovata.",
+        "no-entity-filter-specified": "Nessun filtro di entità specificato",
+        "root-state-entity": "Use dashboard state entity as root",
+        "root-entity": "Entità radice",
+        "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": "Tutte le entità",
+        "any-relation": "qualsiasi"
+    },
+    "asset": {
+        "asset": "Asset",
+        "assets": "Asset",
+        "management": "Gestione Asset",
+        "view-assets": "Visualizza Asset",
+        "add": "Aggiungi Asset",
+        "assign-to-customer": "Assegna a cliente",
+        "assign-asset-to-customer": "Assegna Asset al Cliente",
+        "assign-asset-to-customer-text": "Seleziona gli asset da assegnare al cliente",
+        "no-assets-text": "Nessun asset trovato",
+        "assign-to-customer-text": "Seleziona il cliente a cui assegnare l'asset / gli asset",
+        "public": "Pubblico",
+        "assignedToCustomer": "Assegnato al cliente",
+        "make-public": "Rendi pubblico l'asset",
+        "make-private": "Rendi privato l'asset",
+        "unassign-from-customer": "Assegnazione annullata dal cliente",
+        "delete": "Cancella asset",
+        "asset-public": "L'Asset è pubblico",
+        "asset-type": "Tipo di Asset",
+        "asset-type-required": "Tipo di Asset richiesto.",
+        "select-asset-type": "Seleziona tipo di asset",
+        "enter-asset-type": "Inserisci tipo di asset",
+        "any-asset": "Qualsiasi asset",
+        "no-asset-types-matching": "Nessun asset corrispondente al tipo '{{entitySubtype}}' è stato trovato.",
+        "asset-type-list-empty": "Nessun tipo di asset selezionato.",
+        "asset-types": "Tipi di Asset",
+        "name": "Nome",
+        "name-required": "Nome obbligatorio.",
+        "description": "Descrizione",
+        "type": "Tipo",
+        "type-required": "Tipo obbligatorio.",
+        "details": "Dettagli",
+        "events": "Eventi",
+        "add-asset-text": "Aggiungi un nuovo asset",
+        "asset-details": "Dettagli Asset",
+        "assign-assets": "Assegna asset",
+        "assign-assets-text": "Assegna { count, plural, 1 {1 asset} other {# assets} } al cliente",
+        "delete-assets": "Cancella asset",
+        "unassign-assets": "Annulla assegnazione asset",
+        "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer",
+        "assign-new-asset": "Assegna un nuovo asset",
+        "delete-asset-title": "Sei sicuro di voler cancellare l'asset '{{assetName}}'?",
+        "delete-asset-text": "Attenzione, dopo la conferma l'asset e tutti i relativi dati non saranno più recuperabili.",
+        "delete-assets-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 asset} other {# asset} }?",
+        "delete-assets-action-title": "Elimina { count, plural, 1 {1 asset} other {# asset} }",
+        "delete-assets-text": "Attenzione, dopo la modifica tutti gli asset selezionati saranno rimossi e tutti i relativi dati non saranno più recuperabili.",
+        "make-public-asset-title": "Sei sicuro di voler rendere pubblico l'asset '{{assetName}}'?",
+        "make-public-asset-text": "Dopo la conferma l'asset e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.",
+        "make-private-asset-title": "Sei sicuro di voler rendere privato l'asset '{{assetName}}'?",
+        "make-private-asset-text": "Dopo la conferma l'asset e tutti i suoi dati saranno resi privati e non accessibili dagli altri.",
+        "unassign-asset-title": "Sei sicuro di voler annullare l'assegnazione dell'asset '{{assetName}}'?",
+        "unassign-asset-text": "Dopo la conferma l'assegnazione dell'asset sarà annullata e l'asset non sarà più accessibile dal cliente.",
+        "unassign-asset": "Annulla assegnazione asset",
+        "unassign-assets-title": "Sei sicuro di voler annullare l'assegnazione di { count, plural, 1 {1 asset} other {# asset} }?",
+        "unassign-assets-text": "Dopo la conferma sarà annullata l'assegnazione di tutti gli asset selezionati e questi non saranno più accessibili dal cliente.",
+        "copyId": "Copia Id asset",
+        "idCopiedMessage": "Id Asset copiato negli Appunti",
+        "select-asset": "Seleziona asset",
+        "no-assets-matching": "Nessun asset corrispondente a '{{entity}}' é stato trovato.",
+        "asset-required": "Asset obbligatorio",
+        "name-starts-with": "Asset con nome che inizia per"
+    },
+    "attribute": {
+        "attributes": "Attributi",
+        "latest-telemetry": "Ultima telemetria",
+        "attributes-scope": "Entity attributes scope",
+        "scope-latest-telemetry": "Ultima telemetria",
+        "scope-client": "Attributi client",
+        "scope-server": "Attributi server",
+        "scope-shared": "Attributi condivisi",
+        "add": "Aggiungi attributo",
+        "key": "Chiave",
+        "last-update-time": "Ultimo aggiornamento",
+        "key-required": "Attributo chiave richiesto.",
+        "value": "Valore",
+        "value-required": "Attributo valore richiesto.",
+        "delete-attributes-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 attributo} other {# attributi} }?",
+        "delete-attributes-text": "Attenzione, dopo la conferma tutti gli attributi selezionati saranno rimossi.",
+        "delete-attributes": "Elimina attributi",
+        "enter-attribute-value": "Inserisci il valore dell'attributo",
+        "show-on-widget": "Mostra sul widget",
+        "widget-mode": "Modalità Widget",
+        "next-widget": "Widget successivo",
+        "prev-widget": "Widget precedente",
+        "add-to-dashboard": "Aggiungi alla dashboard",
+        "add-widget-to-dashboard": "Aggiungi widget alla dashboard",
+        "selected-attributes": "{ count, plural, 1 {1 attributo selezionato} other {# attributi selezionati} }",
+        "selected-telemetry": "{ count, plural, 1 {1 unità di telemetria selezionata} other {# unità di telemetria selezionate} }"
+    },
+    "audit-log": {
+        "audit": "Audit",
+        "audit-logs": "Audit Logs",
+        "timestamp": "Timestamp",
+        "entity-type": "Tipo Entità",
+        "entity-name": "Nome Entità",
+        "user": "Utente",
+        "type": "Tipo",
+        "status": "Stato",
+        "details": "Dettagli",
+        "type-added": "Aggiunto",
+        "type-deleted": "Eliminato",
+        "type-updated": "Aggiornato",
+        "type-attributes-updated": "Attributi aggiornati",
+        "type-attributes-deleted": "Attributi eliminati",
+        "type-rpc-call": "Chiamata RPC",
+        "type-credentials-updated": "Credenziali aggiornate",
+        "type-assigned-to-customer": "Assegnato al Cliente",
+        "type-unassigned-from-customer": "Assegnazione annullata dal Cliente",
+        "type-activated": "Attivato",
+        "type-suspended": "Sospeso",
+        "type-credentials-read": "Credenziali lette",
+        "type-attributes-read": "Attributi letti",
+        "status-success": "Success",
+        "status-failure": "Failure",
+        "audit-log-details": "Dettaglio log audit",
+        "no-audit-logs-prompt": "Log non trovati",
+        "action-data": "Action data",
+        "failure-details": "Failure details",
+        "search": "Riceraca log audit",
+        "clear-search": "Cancella ricerca"
+    },
+    "confirm-on-exit": {
+        "message": "Alcune modifiche non sono state salvate. Sei sicuro di voler abbandonare questa pagina?",
+        "html-message": "Alcune modifiche non sono state salvate.<br/>Sei sicuro di voler abbandonare questa pagina?",
+        "title": "Modifiche non salvate"
+    },
+    "contact": {
+        "country": "Nazione",
+        "city": "Città",
+        "state": "Stato / Provincia",
+        "postal-code": "CAP",
+        "postal-code-invalid": "Formato CAP non valido.",
+        "address": "Indirizzo",
+        "address2": "Indirizzo 2",
+        "phone": "Telefono",
+        "email": "Email",
+        "no-address": "Nessun indirizzo"
+    },
+    "common": {
+        "username": "Nome utente",
+        "password": "Password",
+        "enter-username": "Inserisci nome utente",
+        "enter-password": "Inserisci password",
+        "enter-search": "Enter search"
+    },
+    "content-type": {
+        "json": "Json",
+        "text": "Testo",
+        "binary": "Binario (Base64)"
+    },
+    "customer": {
+        "customer": "Cliente",
+        "customers": "Clienti",
+        "management": "Gestione cliente",
+        "dashboard": "Dashboard cliente",
+        "dashboards": "Dashboard cliente",
+        "devices": "Dispositivi cliente",
+        "assets": "Asset cliente",
+        "public-dashboards": "Dashboard pubbliche",
+        "public-devices": "Dispositivi pubblici",
+        "public-assets": "Asset pubblici",
+        "add": "Aggiungi cliente",
+        "delete": "Elimina cliente",
+        "manage-customer-users": "Gestisci utenti cliente",
+        "manage-customer-devices": "Gestisci dispositivi cliente",
+        "manage-customer-dashboards": "Gestisci dashboard cliente",
+        "manage-public-devices": "Gestisci dispositivi pubblici",
+        "manage-public-dashboards": "Gestisci dashboard pubbliche",
+        "manage-customer-assets": "Gestisci asset cliente",
+        "manage-public-assets": "Gestisci asset pubblici",
+        "add-customer-text": "Aggiungi nuovo cliente",
+        "no-customers-text": "Nessun cliente trovato",
+        "customer-details": "Dettagli cliente",
+        "delete-customer-title": "Sei sicuro di voler eliminare il cliente '{{customerTitle}}'?",
+        "delete-customer-text": "Attenzione, dopo la conferma il cliente e tutti i suoi dati non saranno più recuperabili.",
+        "delete-customers-title": "Sei sicuro di voler cancellare { count, plural, 1 {1 cliente} other {# clienti} }?",
+        "delete-customers-action-title": "Elimina { count, plural, 1 {1 cliente} other {# clienti} }",
+        "delete-customers-text": "Attenzione, dopo la conferma tutti i clienti selezionati saranno rimossi e i loro dati non saranno più recuperabili.",
+        "manage-users": "Gestisci utenti",
+        "manage-assets": "Gestisci asset",
+        "manage-devices": "Gestisci dispositivi",
+        "manage-dashboards": "Gestisci dashboard",
+        "title": "Titolo",
+        "title-required": "Titolo obbligatorio.",
+        "description": "Descrizione",
+        "details": "Dettagli",
+        "events": "Eventi",
+        "copyId": "Copia Id cliente",
+        "idCopiedMessage": "Id cliente copiato negli appunti",
+        "select-customer": "Seleziona cliente",
+        "no-customers-matching": "Nessun cliente corrispondente a '{{entity}}' è stato trovato.",
+        "customer-required": "Cliente obbligatorio",
+        "select-default-customer": "Seleziona cliente di default",
+        "default-customer": "Cliente di default",
+        "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level"
+    },
+    "datetime": {
+        "date-from": "Data da",
+        "time-from": "Orario da",
+        "date-to": "Data a",
+        "time-to": "Orario a"
+    },
+    "dashboard": {
+        "dashboard": "Dashboard",
+        "dashboards": "Dashboard",
+        "management": "Gestione Dashboard",
+        "view-dashboards": "Mostra Dashboard",
+        "add": "Aggiungi Dashboard",
+        "assign-dashboard-to-customer": "Assegna Dashboard al cliente",
+        "assign-dashboard-to-customer-text": "Seleziona le dashboard da assegnare al client",
+        "assign-to-customer-text": "Seleziona il cliente a cui assegnare la/le dashboard",
+        "assign-to-customer": "Assegna al cliente",
+        "unassign-from-customer": "Unassign from customer",
+        "make-public": "Rendi pubblica la dashboard",
+        "make-private": "Rendi privata la dashboard",
+        "manage-assigned-customers": "Gestisci i clienti assegnati",
+        "assigned-customers": "Clienti assegnati",
+        "assign-to-customers": "Assegna Dashboard ai Clienti",
+        "assign-to-customers-text": "Seleziona i clienti da assegnare alla/alle dashboard",
+        "unassign-from-customers": "Unassign Dashboard(s) From Customers",
+        "unassign-from-customers-text": "Seleziona i clienti di cui annullare l'assegnazione alla/alle dashboard",
+        "no-dashboards-text": "Nessuna dashboard trovata",
+        "no-widgets": "Nessun widget configurato",
+        "add-widget": "Aggiungi nuovo widget",
+        "title": "Titolo",
+        "select-widget-title": "Seleziona widget",
+        "select-widget-subtitle": "Elenco tipi di widget disponibili",
+        "delete": "Elimina dashboard",
+        "title-required": "Titolo obbligatorio.",
+        "description": "Descrizione",
+        "details": "Dettagli",
+        "dashboard-details": "Dettagli Dashboard",
+        "add-dashboard-text": "Aggiungi nuova dashboard",
+        "assign-dashboards": "Assegna dashboard",
+        "assign-new-dashboard": "Assegna nuova dashboard",
+        "assign-dashboards-text": "Assegna { count, plural, 1 {1 dashboard} other {# dashboard} } ai clienti",
+        "unassign-dashboards-action-text": "Annulla assegnazione { count, plural, 1 {1 dashboard} other {# dashboards} } ai clienti",
+        "delete-dashboards": "Elimina dashboard",
+        "unassign-dashboards": "Annulla assegnazione dashboard",
+        "unassign-dashboards-action-title": "Annulla assegnazione { count, plural, 1 {1 dashboard} other {# dashboards} } al cliente",
+        "delete-dashboard-title": "Sei sicuro di voler cancellare la dashboard '{{dashboardTitle}}'?",
+        "delete-dashboard-text": "Attenzione, dopo la conferma la dashboard e tutti i suoi dati non saranno più recuperabili.",
+        "delete-dashboards-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 dashboard} other {# dashboard} }?",
+        "delete-dashboards-action-title": "Cancella { count, plural, 1 {1 dashboard} other {# dashboard} }",
+        "delete-dashboards-text": "Attenzione, dopo la conferma tutte le dashboard selezionate saranno eliminate e tutti i loro dati non saranno più recuperabili.",
+        "unassign-dashboard-title": "Sei sicuro di voler annullare l'assegnazione della dashboard '{{dashboardTitle}}'?",
+        "unassign-dashboard-text": "Dopo la conferma sarà annullata l'assegnazione della dashboard e questa non sarà più accessibile dal cliente.",
+        "unassign-dashboard": "Annulla assegnazione dashboard",
+        "unassign-dashboards-title": "Sei sicuro di voler annullare l'assegnazione di  { count, plural, 1 {1 dashboard} other {# dashboard} }?",
+        "unassign-dashboards-text": "Dopo la conferma sarà annullata l'assegnazione di tutte le dashboards selezionate e queste non saranno più accessibili dal cliente.",
+        "public-dashboard-title": "La Dashboard è ora pubblica",
+        "public-dashboard-text": "La dashboard <b>{{dashboardTitle}}</b> è ora pubblica e accessibile al <a href='{{publicLink}}' target='_blank'>link</a>:",
+        "public-dashboard-notice": "<b>Nota:</b> Ricorda di rendere pubblici i relativi dispositivi per accedere ai loro dati.",
+        "make-private-dashboard-title": "Sei sicuro di voler rendere privata la dashboard '{{dashboardTitle}}'?",
+        "make-private-dashboard-text": "Dopo la conferma la dashboard sarà resa privata e non più accessibile dagli altri.",
+        "make-private-dashboard": "Rendi privata la dashboard",
+        "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
+        "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
+        "select-dashboard": "Seleziona dashboard",
+        "no-dashboards-matching": "Nessuna dashboard corrispondente a '{{entity}}' è stata trovata.",
+        "dashboard-required": "Dashboard obbligatoria.",
+        "select-existing": "Seleziona una dashboard esistente",
+        "create-new": "Crea nuova dashboard",
+        "new-dashboard-title": "Titolo nuova dashboard",
+        "open-dashboard": "Apri dashboard",
+        "set-background": "Imposta sfondo",
+        "background-color": "Colore sfondo",
+        "background-image": "Immagine sfondo",
+        "background-size-mode": "Background size mode",
+        "no-image": "Nessuna immagine selezionata",
+        "drop-image": "Trascina un'immagine o fai clic per selezionare un file da caricare.",
+        "settings": "Impostazioni",
+        "columns-count": "Numero colonne",
+        "columns-count-required": "Numero colonne obbligatorio.",
+        "min-columns-count-message": "Ammesso un numero minimo di colonne pari a 10.",
+        "max-columns-count-message": "Ammesso un numero massimo di colonne pari a 1000.",
+        "widgets-margins": "Margine tra i widget",
+        "horizontal-margin": "Margine orizzontale",
+        "horizontal-margin-required": "Margine orizzontale obbligatorio.",
+        "min-horizontal-margin-message": "Ammesso un margine orizzontale minimo pari a 0.",
+        "max-horizontal-margin-message": "Ammesso un margine orizzontale massimo pari a 50.",
+        "vertical-margin": "Margine verticale",
+        "vertical-margin-required": "Margine verticale obbligatorio.",
+        "min-vertical-margin-message": "Ammesso un margine verticale minimo pari a 0.",
+        "max-vertical-margin-message": "Ammesso un margine verticale massimo pari a 50.",
+        "autofill-height": "Auto fill layout height",
+        "mobile-layout": "Impostazioni layout mobile",
+        "mobile-row-height": "Mobile row height, px",
+        "mobile-row-height-required": "Mobile row height value is required.",
+        "min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.",
+        "max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.",
+        "display-title": "Mostra titolo dashboard",
+        "toolbar-always-open": "Mantieni aperta la barra degli strumenti",
+        "title-color": "Colore titolo",
+        "display-dashboards-selection": "Mostra selezione dashboard",
+        "display-entities-selection": "Mostra selezione entità",
+        "display-dashboard-timewindow": "Display timewindow",
+        "display-dashboard-export": "Mostra esportazione",
+        "import": "Importa dashboard",
+        "export": "Esporta dashboard",
+        "export-failed-error": "Impossibile esportare la dashboard: {{error}}",
+        "create-new-dashboard": "Crea nuova dashboard",
+        "dashboard-file": "File dashboard",
+        "invalid-dashboard-file-error": "Impossibile importare la dashboard: struttura dati della dashboard non valida.",
+        "dashboard-import-missing-aliases-title": "Configura alias utilizzati dalla dashboard importata",
+        "create-new-widget": "Crea nuovo widget",
+        "import-widget": "Importa widget",
+        "widget-file": "Widget file",
+        "invalid-widget-file-error": "Impossibile importare il widget: struttura dati del widget non valida.",
+        "widget-import-missing-aliases-title": "Configura gli alias utilizzati dai widget importati",
+        "open-toolbar": "Apri barra degli strumenti",
+        "close-toolbar": "Chiudi barra degli strumenti",
+        "configuration-error": "Errore di configurazione",
+        "alias-resolution-error-title": "Errore di configurazione degli alias della dashboard",
+        "invalid-aliases-config": "Impossibile trovare un dispositivo corrispondente ad un qualche filtro degli alias.<br/>Contatta l'amministratore per risolvere il problema.",
+        "select-devices": "Seleziona dispositivi",
+        "assignedToCustomer": "Assegnato al cliente",
+        "assignedToCustomers": "Assegnato ai clienti",
+        "public": "Pubblico",
+        "public-link": "Link pubblico",
+        "copy-public-link": "Copia link pubblico",
+        "public-link-copied-message": "Link pubblico della dashboard copiato negli appunti",
+        "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": "Nome",
+        "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": "Mostra dettagli",
+        "hide-details": "Nascondi dettagli",
+        "select-state": "Select target state",
+        "state-controller": "Stato controller"
+    },
+    "datakey": {
+        "settings": "Impostazioni",
+        "advanced": "Avanzate",
+        "label": "Etichetta",
+        "color": "Colore",
+        "units": "Simbolo speciale da mostrare accanto al valore",
+        "decimals": "Numero cifre decimali",
+        "data-generation-func": "Funzione generazione dati",
+        "use-data-post-processing-func": "Use data post-processing function",
+        "configuration": "Data key configuration",
+        "timeseries": "Serie temporali",
+        "attributes": "Attributi",
+        "alarm": "Campi allarme",
+        "timeseries-required": "Entity timeseries are required.",
+        "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
+        "maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }",
+        "alarm-fields-required": "Campi allarme obbligatori.",
+        "function-types": "Tipi funzione",
+        "function-types-required": "Tipi funzione obbligatorio.",
+        "maximum-function-types": "Massimo { count, plural, 1 {1 tipo di funzione consentito.} other {# tipi di funzione consentiti} }"
+    },
+    "datasource": {
+        "type": "Tipo sorgente dati",
+        "name": "Nome",
+        "add-datasource-prompt": "Aggiungi una sorgente dati"
+    },
+    "details": {
+        "edit-mode": "Modalità modifica",
+        "toggle-edit-mode": "Toggle edit mode"
+    },
+    "device": {
+        "device": "Dispositivo",
+        "device-required": "Dispositivo richiesto.",
+        "devices": "Dispositivi",
+        "management": "Gestione dispositivo",
+        "view-devices": "Visualizza Dispositivi",
+        "device-alias": "Alias dispositivo",
+        "aliases": "Alias dispositivo",
+        "no-alias-matching": "'{{alias}}' non trovato.",
+        "no-aliases-found": "Nessun alias trovato.",
+        "no-key-matching": "'{{key}}' non trovata.",
+        "no-keys-found": "Nessuna chiave trovata.",
+        "create-new-alias": "Create a new one!",
+        "create-new-key": "Create a new one!",
+        "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.",
+        "configure-alias": "Configura alias '{{alias}}'",
+        "no-devices-matching": "Nessun dispositivo corrispondente a '{{entity}}' é stato trovato.",
+        "alias": "Alias",
+        "alias-required": "Alias dispositivo richesto.",
+        "remove-alias": "Rimuovi alias dispositivo",
+        "add-alias": "Aggiungi alias dispositivo",
+        "name-starts-with": "Device name starts with",
+        "device-list": "Lista dispositivi",
+        "use-device-name-filter": "Usa filtro",
+        "device-list-empty": "Nessun dispositivo selezionato.",
+        "device-name-filter-required": "Device name filter is required.",
+        "device-name-filter-no-device-matched": "No devices starting with '{{device}}' were found.",
+        "add": "Aggiungi Dispositivo",
+        "assign-to-customer": "Assigna al cliente",
+        "assign-device-to-customer": "Assegna dispositivo/dispositivi al Cliente",
+        "assign-device-to-customer-text": "Seleziona i dispositivi da assegnare al cliente",
+        "make-public": "Rendi pubblico il dispositivo",
+        "make-private": "rendi privato il dispositivo",
+        "no-devices-text": "Nessun dispositivo trovato",
+        "assign-to-customer-text": "Seleziona il cliente a cui assegnare il dispositivo/i dispositivi",
+        "device-details": "Dettagli dispositivo",
+        "add-device-text": "Aggiungi nuovo dispositivo",
+        "credentials": "Credenziali",
+        "manage-credentials": "Gestisci credenziali",
+        "delete": "Elimina dispositivo",
+        "assign-devices": "Assegna dispositivi",
+        "assign-devices-text": "Assegna { count, plural, 1 {1 dispositivo} other {# dispositivi} } al cliente",
+        "delete-devices": "Elimina dispositivi",
+        "unassign-from-customer": "Annulla assegnazione al cliente",
+        "unassign-devices": "Annulla assegnazione dispositivi",
+        "unassign-devices-action-title": "Annulla assegnazione { count, plural, 1 {1 dispositivo} other {# dispositivi} } al cliente",
+        "assign-new-device": "Assegna nuovo dispositivo",
+        "make-public-device-title": "Sei sicuro di voler rendere pubblico il dispositivo '{{deviceName}}'?",
+        "make-public-device-text": "Dopo la conferma il dispositivo e tutti i suoi dati saranno resi pubblici e accessibili dagli altri.",
+        "make-private-device-title": "Sei sicuro di voler rendere privato il dispositivo '{{deviceName}}'?",
+        "make-private-device-text": "Dopo la conferma il dispositivo e tutti i suoi dati saranno resi privati e non più accessibili da altri utenti.",
+        "view-credentials": "Visualizza credenziali",
+        "delete-device-title": "Sei sicuro di voler eliminare il dispositivo '{{deviceName}}'?",
+        "delete-device-text": "Attenzione, dopo la conferma il dispositivo e tutti i suoi dati non saranno più recuperabili.",
+        "delete-devices-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 dispositivo} other {# dispositivi} }?",
+        "delete-devices-action-title": "Elimina { count, plural, 1 {1 dispositivo} other {# dispositivi} }",
+        "delete-devices-text": "Attenzione, dopo la conferma tutti i dispositivi selezionati saranno elimininati e i relativi dati non saranno più recuperabili.",
+        "unassign-device-title": "Sei sicuro di voler annullare l'assegnazione del dispositivo '{{deviceName}}'?",
+        "unassign-device-text": "Dopo la conferma sarà annullata l'assegnazione del dispositivo e questo non sarà più accessibile dal cliente.",
+        "unassign-device": "Annulla assegnazione dispositivo",
+        "unassign-devices-title": "Sei sicuro di voler annullare la'ssegnazione di { count, plural, 1 {1 dispositivo} other {# dispositivi} }?",
+        "unassign-devices-text": "Dopo la conferma sarà annullata l'assegnazione di tutti i dispositivi selezionati e questi non saranno più accessibili dal cliente.",
+        "device-credentials": "Credenziali Dispositivo",
+        "credentials-type": "Tipo credenziali",
+        "access-token": "Token di accesso",
+        "access-token-required": "Token di accesso obbligatorio.",
+        "access-token-invalid": "Il token di accesso deve avere una lunghezza compresa tra 1 e 20 caratteri.",
+        "rsa-key": "Chiave pubblica RSA",
+        "rsa-key-required": "Chiave pubblica RSA obbligatoria.",
+        "secret": "Secret",
+        "secret-required": "Secret obbligatorio.",
+        "device-type": "Tipo dispositivo",
+        "device-type-required": "Tipo dispositivo obbligatorio.",
+        "select-device-type": "Seleziona tipo dispositivo",
+        "enter-device-type": "Inserisci typo dispositivo",
+        "any-device": "Qualsiasi dispositivo",
+        "no-device-types-matching": "Nessun dispositivo corrispondente a '{{entitySubtype}}' è stato trovato.",
+        "device-type-list-empty": "Nessun tipo di dispositivo selezionato.",
+        "device-types": "Tipi dispositivo",
+        "name": "Nome",
+        "name-required": "Nome obbligatorio.",
+        "description": "Descrizione",
+        "events": "Eventi",
+        "details": "Dettagli",
+        "copyId": "Copia Id dispositivo",
+        "copyAccessToken": "Copia token di accesso",
+        "idCopiedMessage": "Id dispositivo copiato negli Appunti",
+        "accessTokenCopiedMessage": "Token di accesso del dispositivo copiato negli Appunti",
+        "assignedToCustomer": "Assegnato al cliente",
+        "unable-delete-device-alias-title": "Impossibile rimuovere l'alias del dispositivo",
+        "unable-delete-device-alias-text": "L'alias del dispositivo '{{deviceAlias}}' non può essere eliminato perchè utilizzato dai seguenti widget:<br/>{{widgetsList}}",
+        "is-gateway": "E' un gateway",
+        "public": "Pubblico",
+        "device-public": "Il dispositivo è pubblico",
+        "select-device": "Seleziona dispositivo"
+    },
+    "dialog": {
+        "close": "Close dialog"
+    },
+    "error": {
+        "unable-to-connect": "Impossibile connettersi al server! Controlla la connessione ad Internet.",
+        "unhandled-error-code": "Codice errore non gestito: {{errorCode}}",
+        "unknown-error": "Errore sconosciuto"
+    },
+    "entity": {
+        "entity": "Entità",
+        "entities": "Entità",
+        "aliases": "Alias entità",
+        "entity-alias": "Alias entità",
+        "unable-delete-entity-alias-title": "Impossibile eliminare alias entità",
+        "unable-delete-entity-alias-text": "L'alias dell'entità '{{entityAlias}}' non può essere eliminato perchè utilizzato dai seguenti widget:<br/>{{widgetsList}}",
+        "duplicate-alias-error": "Trovato un duplicato dell'alias '{{alias}}'.<br>Gli alias dell'entità devono essere univoci all'interno della dashboard.",
+        "missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.",
+        "configure-alias": "Configura '{{alias}}' alias",
+        "alias": "Alias",
+        "alias-required": "Alias entità obbligatorio.",
+        "remove-alias": "Rimuovi alias entità",
+        "add-alias": "Aggiungi alias entità",
+        "entity-list": "Lista entità",
+        "entity-type": "Tipo entità",
+        "entity-types": "Tipi entità",
+        "entity-type-list": "Lista tipo entità",
+        "any-entity": "Qualsiasi entità",
+        "enter-entity-type": "Inserisci tipo entità",
+        "no-entities-matching": "Nessuna entità corrispondente a '{{entity}}' è stata trovata.",
+        "no-entity-types-matching": "Nessun tipo di entità corrispondente a '{{entityType}}' è stato trovato.",
+        "name-starts-with": "Nome inizia per",
+        "use-entity-name-filter": "Usa filtro",
+        "entity-list-empty": "Nessuna entità selezionata.",
+        "entity-type-list-empty": "Nessun tipo di entità selezionato.",
+        "entity-name-filter-required": "Filtro nome entità obbligatorio.",
+        "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
+        "all-subtypes": "Tutte",
+        "select-entities": "Seleziona entità",
+        "no-aliases-found": "Nessun alias trovato.",
+        "no-alias-matching": "'{{alias}}' non trovato.",
+        "create-new-alias": "Create a new one!",
+        "key": "Chiave",
+        "key-name": "Nome chiave",
+        "no-keys-found": "Nessuna chiave trovata.",
+        "no-key-matching": "'{{key}}' non trovata.",
+        "create-new-key": "Create a new one!",
+        "type": "Tipo",
+        "type-required": "Tipo entità obbligatorio.",
+        "type-device": "Dispositivo",
+        "type-devices": "Dispositivi",
+        "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista di # dispositivi} }",
+        "device-name-starts-with": "Dispositivi i cui nomi iniziano per '{{prefix}}'",
+        "type-asset": "Asset",
+        "type-assets": "Asset",
+        "list-of-assets": "{ count, plural, 1 {Un asset} other {Lista di # asset} }",
+        "asset-name-starts-with": "Asset i cui nomi iniziano per '{{prefix}}'",
+        "type-rule": "Regola",
+        "type-rules": "Regole",
+        "list-of-rules": "{ count, plural, 1 {Una regola} other {Lista di # regole} }",
+        "rule-name-starts-with": "Regole i cui nomi iniziano per '{{prefix}}'",
+        "type-plugin": "Plugin",
+        "type-plugins": "Plugin",
+        "list-of-plugins": "{ count, plural, 1 {Un plugin} other {Lista di # plugin} }",
+        "plugin-name-starts-with": "Plugin i cui nomi iniziano per '{{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": "Cliente",
+        "type-customers": "Clienti",
+        "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista di # clienti} }",
+        "customer-name-starts-with": "Clienti i cui nomi iniziano per '{{prefix}}'",
+        "type-user": "Utente",
+        "type-users": "Utenti",
+        "list-of-users": "{ count, plural, 1 {Un utente} other {Lista of # utenti} }",
+        "user-name-starts-with": "Utenti i cui nomi iniziano per '{{prefix}}'",
+        "type-dashboard": "Dashboard",
+        "type-dashboards": "Dashboard",
+        "list-of-dashboards": "{ count, plural, 1 {Una dashboard} other {Lista di # dashboard} }",
+        "dashboard-name-starts-with": "Dashboard i cui nomi iniziano per '{{prefix}}'",
+        "type-alarm": "Allarme",
+        "type-alarms": "Allarmi",
+        "list-of-alarms": "{ count, plural, 1 {Un allarme} other {Lista di # allarmi} }",
+        "alarm-name-starts-with": "Allarmi i cui nomi iniziano per '{{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": "Ricerca entità",
+        "selected-entities": "{ count, plural, 1 {1 entità selezionata} other {# entità selezionate} }",
+        "entity-name": "Nome entità",
+        "details": "Dettagli entità",
+        "no-entities-prompt": "Nessuna entità trovata",
+        "no-data": "Nessun dato da mostrare"
+    },
+    "event": {
+        "event-type": "Tipo evento",
+        "type-error": "Errore",
+        "type-lc-event": "Ciclo di vita evento",
+        "type-stats": "Statistiche",
+        "type-debug-rule-node": "Debug",
+        "type-debug-rule-chain": "Debug",
+        "no-events-prompt": "Nessun evento trovato",
+        "error": "Errore",
+        "alarm": "Allarme",
+        "event-time": "Orario evento",
+        "server": "Server",
+        "body": "Body",
+        "method": "Metodo",
+        "type": "Tipo",
+        "entity": "Entità",
+        "message-id": "Id Messaggio",
+        "message-type": "Tipo Messaggio",
+        "data-type": "Data Type",
+        "relation-type": "Tipo di relazione",
+        "metadata": "Metadati",
+        "data": "Dati",
+        "event": "Evento",
+        "status": "Stato",
+        "success": "Success",
+        "failed": "Failed",
+        "messages-processed": "Messaggi elaborati",
+        "errors-occurred": "Si sono verificati degli errori"
+    },
+    "extension": {
+        "extensions": "Estensioni",
+        "selected-extensions": "{ count, plural, 1 {1 estensione selezionata} other {# estensioni selezionate} }",
+        "type": "Tipo",
+        "key": "Chiave",
+        "value": "Valore",
+        "id": "Id",
+        "extension-id": "Id Estensione",
+        "extension-type": "Tipo Estensione",
+        "transformer-json": "JSON *",
+        "unique-id-required": "Id estensione corrente già esistente.",
+        "delete": "Elimina estensione",
+        "add": "Aggiungi estensione",
+        "edit": "Modifica estensione",
+        "delete-extension-title": "Sei sicuro di voler eliminare l'estensione '{{extensionId}}'?",
+        "delete-extension-text": "Attenzione, dopo la conferma l'estensione e tutti i suoi data non saranno più recuperabili.",
+        "delete-extensions-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 estensione} other {# estensioni} }?",
+        "delete-extensions-text": "Attenzione, dopo la conferma tutte le estensioni selezionate saranno eliminate.",
+        "converters": "Converters",
+        "converter-id": "Converter id",
+        "configuration": "Configurazione",
+        "converter-configurations": "Converter configurations",
+        "token": "Token di sicurezza",
+        "add-converter": "Add converter",
+        "add-config": "Add converter configuration",
+        "device-name-expression": "Device name expression",
+        "device-type-expression": "Device type expression",
+        "custom": "Custom",
+        "to-double": "To Double",
+        "transformer": "Transformer",
+        "json-required": "Transformer json is required.",
+        "json-parse": "Unable to parse transformer json.",
+        "attributes": "Attributi",
+        "add-attribute": "Aggiungi attributo",
+        "add-map": "Add mapping element",
+        "timeseries": "Serie temporali",
+        "add-timeseries": "Add timeseries",
+        "field-required": "Field is required",
+        "brokers": "Broker",
+        "add-broker": "Aggiungi broker",
+        "host": "Host",
+        "port": "Porta",
+        "port-range": "Port should be in a range from 1 to 65535.",
+        "ssl": "Ssl",
+        "credentials": "Credenziali",
+        "username": "Nome utente",
+        "password": "Password",
+        "retry-interval": "Retry interval in milliseconds",
+        "anonymous": "Anonimo",
+        "basic": "Basic",
+        "pem": "PEM",
+        "ca-cert": "CA certificate file *",
+        "private-key": "File chiave privata *",
+        "cert": "File certificato *",
+        "no-file": "Nessun file selezionato.",
+        "drop-file": "Trascina un file o fai clic per selezionare un file da caricare.",
+        "mapping": "Mapping",
+        "topic-filter": "Filtro topic",
+        "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 millisecondi",
+        "converter-json-required": "Convertitore json obbligatorio.",
+        "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": "Aggiungi dispositivo",
+        "opc-server": "Server",
+        "opc-add-server": "Aggiungi server",
+        "opc-add-server-prompt": "Aggiungi server",
+        "opc-application-name": "Nome applicazione",
+        "opc-application-uri": "Uri applicazione",
+        "opc-scan-period-in-seconds": "Intervallo di scansione in secondi",
+        "opc-security": "Sicurezza",
+        "opc-identity": "Identità",
+        "opc-keystore": "Keystore",
+        "opc-type": "Tipo",
+        "opc-keystore-type": "Tipo",
+        "opc-keystore-location": "Location *",
+        "opc-keystore-password": "Password",
+        "opc-keystore-alias": "Alias",
+        "opc-keystore-key-password": "Chiave password",
+        "opc-device-node-pattern": "Device node pattern",
+        "opc-device-name-pattern": "Device name pattern",
+        "modbus-server": "Servers/slaves",
+        "modbus-add-server": "Aggiungi server/slave",
+        "modbus-add-server-prompt": "Aggiungi server/slave",
+        "modbus-transport": "Transport",
+        "modbus-port-name": "Nome porta seriale",
+        "modbus-encoding": "Codifica",
+        "modbus-parity": "Parità",
+        "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": "Nome dispositivo",
+        "modbus-poll-period": "Intervallo di polling (ms)",
+        "modbus-attributes-poll-period": "Attributes poll period (ms)",
+        "modbus-timeseries-poll-period": "Timeseries poll period (ms)",
+        "modbus-poll-period-range": "L'intervallo di polling deve essere un valore positivo.",
+        "modbus-tag": "Tag",
+        "modbus-function": "Funzione",
+        "modbus-register-address": "Indirizzo registro",
+        "modbus-register-address-range": "L'indirizzo del registro deve essere compreso tra 0 e 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",
+
+        "sync": {
+            "status": "Stato",
+            "sync": "Sincronizzato",
+            "not-sync": "Non sincronizzato",
+            "last-sync-time": "Ultima sincronizzazione",
+            "not-available": "Non disponibile"
+        },
+
+        "export-extensions-configuration": "Export extensions configuration",
+        "import-extensions-configuration": "Import extensions configuration",
+        "import-extensions": "Importa estensione",
+        "import-extension": "Importa estensione",
+        "export-extension": "Esporta estensione",
+        "file": "File estensione",
+        "invalid-file-error": "File estensione non valido"
+    },
+    "fullscreen": {
+        "expand": "Expand to fullscreen",
+        "exit": "Exit fullscreen",
+        "toggle": "Toggle fullscreen mode",
+        "fullscreen": "Fullscreen"
+    },
+    "function": {
+        "function": "Function"
+    },
+    "grid": {
+        "delete-item-title": "Are you sure you want to delete this item?",
+        "delete-item-text": "Be careful, after the confirmation this item and all related data will become unrecoverable.",
+        "delete-items-title": "Are you sure you want to delete { count, plural, 1 {1 item} other {# items} }?",
+        "delete-items-action-title": "Delete { count, plural, 1 {1 item} other {# items} }",
+        "delete-items-text": "Be careful, after the confirmation all selected items will be removed and all related data will become unrecoverable.",
+        "add-item-text": "Add new item",
+        "no-items-text": "No items found",
+        "item-details": "Item details",
+        "delete-item": "Delete Item",
+        "delete-items": "Delete Items",
+        "scroll-to-top": "Scroll to top"
+    },
+    "help": {
+        "goto-help-page": "Go to help page"
+    },
+    "home": {
+        "home": "Home",
+        "profile": "Profilo",
+        "logout": "Logout",
+        "menu": "Menu",
+        "avatar": "Avatar",
+        "open-user-menu": "Apri menu utente"
+    },
+    "import": {
+        "no-file": "Nessun file selezionato",
+        "drop-file": "Trascina un file JSON o fai clic per selezionare un file da caricare."
+    },
+    "item": {
+        "selected": "Selezionata"
+    },
+    "js-func": {
+        "no-return-error": "La funzione deve restituire un valore!",
+        "return-type-mismatch": "La funzione deve restituire un valore di tipo '{{type}}'!",
+        "tidy": "Tidy"
+    },
+    "key-val": {
+        "key": "Chiave",
+        "value": "Valore",
+        "remove-entry": "Remove entry",
+        "add-entry": "Add entry",
+        "no-data": "No entries"
+    },
+    "layout": {
+        "layout": "Layout",
+        "manage": "Gestisci layout",
+        "settings": "Impostazioni layout",
+        "color": "Colore",
+        "main": "Main",
+        "right": "Right",
+        "select": "Select target layout"
+    },
+    "legend": {
+        "position": "Posizione Legenda",
+        "show-max": "Mostra valore max",
+        "show-min": "Mostra valore min",
+        "show-avg": "Mostra valore medio",
+        "show-total": "Mostra valore totale",
+        "settings": "Impostazioni legenda",
+        "min": "min",
+        "max": "max",
+        "avg": "avg",
+        "total": "totale"
+    },
+    "login": {
+        "login": "Login",
+        "request-password-reset": "Request Password Reset",
+        "reset-password": "Azzera Password",
+        "create-password": "Crea Password",
+        "passwords-mismatch-error": "Le password inserite devono corrispondere!",
+        "password-again": "Ripeti Password",
+        "sign-in": "Please sign in",
+        "username": "Nome utente (email)",
+        "remember-me": "Ricordami",
+        "forgot-password": "Password dimenticata?",
+        "password-reset": "Password reset",
+        "new-password": "Nuova password",
+        "new-password-again": "Ripeti nuova password",
+        "password-link-sent-message": "Link azzeramento password inviato con successo!",
+        "email": "Email"
+    },
+    "position": {
+        "top": "Alto",
+        "bottom": "Basso",
+        "left": "Sinistra",
+        "right": "Destra"
+    },
+    "profile": {
+        "profile": "Profilo",
+        "change-password": "Modifica Password",
+        "current-password": "Password attuale"
+    },
+    "relation": {
+        "relations": "Relations",
+        "direction": "Direction",
+        "search-direction": {
+            "FROM": "Da",
+            "TO": "A"
+        },
+        "direction-type": {
+            "FROM": "da",
+            "TO": "a"
+        },
+        "from-relations": "Outbound relations",
+        "to-relations": "Inbound relations",
+        "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} } selected",
+        "type": "Tipo",
+        "to-entity-type": "A tipo entità",
+        "to-entity-name": "A nome entità",
+        "from-entity-type": "Da tipo entità",
+        "from-entity-name": "Da nome entità",
+        "to-entity": "A entità",
+        "from-entity": "Da entità",
+        "delete": "Delete relation",
+        "relation-type": "Relation type",
+        "relation-type-required": "Relation type is required.",
+        "any-relation-type": "Ogni tipo",
+        "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": "Attenzione, dopo la conferma l'entità '{{entityName}}' sarà scollegata dall'entità corrente.",
+        "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."
+    },
+    "rulechain": {
+        "rulechain": "Rule chain",
+        "rulechains": "Rule chains",
+        "root": "Root",
+        "delete": "Delete rule chain",
+        "name": "Nome",
+        "name-required": "Nome obbligatorio.",
+        "description": "Descrizione",
+        "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": "Dettagli",
+        "events": "Eventi",
+        "system": "Sistema",
+        "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": "Modalità debug"
+    },
+    "rulenode": {
+        "details": "Dettagli",
+        "events": "Eventi",
+        "search": "Ricerca nodi",
+        "open-node-library": "Apri libreria nodi",
+        "add": "Add rule node",
+        "name": "Nome",
+        "name-required": "Nome obbligatorio.",
+        "type": "Tipo",
+        "description": "Descrizione",
+        "delete": "Delete rule node",
+        "select-all-objects": "Seleziona tutti i nodi e le connessioni",
+        "deselect-all-objects": "Deselect all nodes and connections",
+        "delete-selected-objects": "Cancella nodi e connessioni selezionate",
+        "delete-selected": "Delete selected",
+        "select-all": "Seleziona tutto",
+        "copy-selected": "Copy selected",
+        "deselect-all": "Deselect all",
+        "rulenode-details": "Rule node details",
+        "debug-mode": "Modalità debug",
+        "configuration": "Configurazione",
+        "link": "Link",
+        "link-details": "Rule node link details",
+        "add-link": "Aggiungi link",
+        "link-label": "Etichetta link",
+        "link-label-required": "Etichetta link obbligatoria.",
+        "custom-link-label": "Custom link label",
+        "custom-link-label-required": "Custom link label is required.",
+        "type-filter": "Filtro",
+        "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": "Azioni",
+        "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": "Messaggio",
+        "message-type": "Tipo messaggio",
+        "message-type-required": "Tipo messaggio obbligatorio",
+        "metadata": "Metadata",
+        "metadata-required": "Metadata entries can't be empty.",
+        "output": "Output",
+        "test": "Test",
+        "help": "Aiuto"
+    },
+    "tenant": {
+        "tenant": "Tenant",
+        "tenants": "Tenant",
+        "management": "Gestione Tenant",
+        "add": "Aggiungi Tenant",
+        "admins": "Amministratori",
+        "manage-tenant-admins": "Gestisci amministratori tenant",
+        "delete": "Cancella tenant",
+        "add-tenant-text": "Aggiungi nuovo tenant",
+        "no-tenants-text": "Nessun tenant trovato",
+        "tenant-details": "Dettagli tenant",
+        "delete-tenant-title": "Sei sicuro di voler eliminare il tenant '{{tenantTitle}}'?",
+        "delete-tenant-text": "Attenzione, dopo la conferma il tenant e tutti i suoi dati non saranno più recuperabili.",
+        "delete-tenants-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 tenant} other {# tenant} }?",
+        "delete-tenants-action-title": "Elimina { count, plural, 1 {1 tenant} other {# tenant} }",
+        "delete-tenants-text": "Attenzione, dopo la conferma tutti i tenant selezionati saranno eliminati e tutti i loro dati non saranno più recuperabili.",
+        "title": "Titolo",
+        "title-required": "Titolo obbligatorio.",
+        "description": "Descrizione",
+        "details": "Dettagli",
+        "events": "Eventi",
+        "copyId": "Copia Id tenant",
+        "idCopiedMessage": "Id tenant copiato negli appunti",
+        "select-tenant": "Seleziona tenant",
+        "no-tenants-matching": "Nessun tenant corrispondente a '{{entity}}' è stato trovato.",
+        "tenant-required": "Tenant obbligatorio"
+    },
+    "timeinterval": {
+        "seconds-interval": "{ seconds, plural, 1 {1 secondo} other {# secondi} }",
+        "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minuti} }",
+        "hours-interval": "{ hours, plural, 1 {1 ora} other {# ore} }",
+        "days-interval": "{ days, plural, 1 {1 giorno} other {# giorni} }",
+        "days": "Giorni",
+        "hours": "Ore",
+        "minutes": "Minuti",
+        "seconds": "Secondi",
+        "advanced": "Avanzate"
+    },
+    "timewindow": {
+        "days": "{ days, plural, 1 { giorno } other {# giorni } }",
+        "hours": "{ hours, plural, 0 { hour } 1 {1 ora } other {# ore } }",
+        "minutes": "{ minutes, plural, 0 { minute } 1 {1 minuto } other {# minuti } }",
+        "seconds": "{ seconds, plural, 0 { second } 1 {1 secondo } other {# secondi } }",
+        "realtime": "Realtime",
+        "history": "Cronologia",
+        "last-prefix": "last",
+        "period": "from {{ startTime }} to {{ endTime }}",
+        "edit": "Edit timewindow",
+        "date-range": "Date range",
+        "last": "Last",
+        "time-period": "Time period"
+    },
+    "user": {
+        "user": "Utente",
+        "users": "Utenti",
+        "customer-users": "Customer Users",
+        "tenant-admins": "Amministratori Tenant",
+        "sys-admin": "Amministratore di sistema",
+        "tenant-admin": "Amministratore tenant",
+        "customer": "Cliente",
+        "anonymous": "Anonimo",
+        "add": "Aggiungi Utente",
+        "delete": "Elimina utente",
+        "add-user-text": "Aggiungi nuovo utente",
+        "no-users-text": "Nessun utente trovato",
+        "user-details": "Dettagli utente",
+        "delete-user-title": "Sei sicuro di voler eliminare l'utente '{{userEmail}}'?",
+        "delete-user-text": "Attenzione, dopo la conferma l'utente e tutti i suoi dati non saranno più recuperabili.",
+        "delete-users-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 utente} other {# utenti} }?",
+        "delete-users-action-title": "Elimina { count, plural, 1 {1 utente} other {# utenti} }",
+        "delete-users-text": "Attenzione, dopo la conferma tutti gli utenti selezionati saranno eliminati e tutti i relativi dati non saranno più recuperabili.",
+        "activation-email-sent-message": "Email di attivazione inviata con successo!",
+        "resend-activation": "Resend activation",
+        "email": "Email",
+        "email-required": "Email obbligatoria.",
+        "invalid-email-format": "Formato email non valido.",
+        "first-name": "Nome",
+        "last-name": "Cognome",
+        "description": "Descrizione",
+        "default-dashboard": "Dashboard di default",
+        "always-fullscreen": "Always fullscreen",
+        "select-user": "Seleziona utente",
+        "no-users-matching": "Nessun utente corrispondente a '{{entity}}' è stato trovato.",
+        "user-required": "Utente obbligatorio",
+        "activation-method": "Metodo di attivazione",
+        "display-activation-link": "Mostra link di attivazione",
+        "send-activation-mail": "Invia email di attivazione",
+        "activation-link": "Link attivazione utente",
+        "activation-link-text": "Per attivare l'utente utilizza il seguente <a href='{{activationLink}}' target='_blank'>link di attivazione</a> :",
+        "copy-activation-link": "Copia link di attivazione",
+        "activation-link-copied-message": "Link di attivazione utente copiato negli appunti",
+        "details": "Dettagli"
+    },
+    "value": {
+        "type": "Tipo valore",
+        "string": "String",
+        "string-value": "Valore string",
+        "integer": "Integer",
+        "integer-value": "Valore integer",
+        "invalid-integer-value": "Valore integer non valido",
+        "double": "Double",
+        "double-value": "Valore double",
+        "boolean": "Boolean",
+        "boolean-value": "Valore boolean",
+        "false": "Falso",
+        "true": "Vero",
+        "long": "Long"
+    },
+    "widget": {
+        "widget-library": "Libreria Widget",
+        "widget-bundle": "Bundle widget",
+        "select-widgets-bundle": "Seleziona bundle widget",
+        "management": "Gestione widget",
+        "editor": "Editor Widget",
+        "widget-type-not-found": "Problem loading widget configuration.<br>Probably associated\n    widget type was removed.",
+        "widget-type-load-error": "Widget non caricato a causa dei seguenti errori:",
+        "remove": "Elimina widget",
+        "edit": "Modifica widget",
+        "remove-widget-title": "sei sicuro di voler eliminare il widget '{{widgetTitle}}'?",
+        "remove-widget-text": "Dopo la conferma il widget e tutti i suoi dati non saranno più recuperabili.",
+        "timeseries": "Time series",
+        "search-data": "Cerca dati",
+        "no-data-found": "Nessun dato trovato",
+        "latest-values": "Ultimi valori",
+        "rpc": "Control widget",
+        "alarm": "Alarm widget",
+        "static": "Static widget",
+        "select-widget-type": "Seleziona tipo widget",
+        "missing-widget-title-error": "Il tiolo del widget deve essere specificato!",
+        "widget-saved": "Widget salvato",
+        "unable-to-save-widget-error": "Impossibile salvare il widget! Sono presenti degli errori!",
+        "save": "Salva widget",
+        "saveAs": "Salva widget come",
+        "save-widget-type-as": "Salva tipo widget come",
+        "save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle",
+        "toggle-fullscreen": "Toggle fullscreen",
+        "run": "Esegui widget",
+        "title": "Titolo widget",
+        "title-required": "Titolo widget obbligatorio.",
+        "type": "Tipo widget",
+        "resources": "Risorse",
+        "resource-url": "JavaScript/CSS URL",
+        "remove-resource": "Rimuovi risorsa",
+        "add-resource": "Aggiungi risorsa",
+        "html": "HTML",
+        "tidy": "Tidy",
+        "css": "CSS",
+        "settings-schema": "Impostazioni schema",
+        "datakey-settings-schema": "Data key settings schema",
+        "javascript": "Javascript",
+        "remove-widget-type-title": "Sei sicuro di voler rimuovere il tipo di widget '{{widgetName}}'?",
+        "remove-widget-type-text": "Dopo la conferma il tipo di widget e tutti i suoi dati non saranno più recuperabili.",
+        "remove-widget-type": "Rimuovi tipo widget",
+        "add-widget-type": "Aggiungi nuovo tipo widget",
+        "widget-type-load-failed-error": "Caricamento tipo widget fallito!",
+        "widget-template-load-failed-error": "Caricamento template widget fallito!",
+        "add": "Aggiungi Widget",
+        "undo": "Annulla modifiche widget",
+        "export": "Esporta widget"
+    },
+    "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)"
+    },
+    "widgets-bundle": {
+        "current": "Bundle corrente",
+        "widgets-bundles": "Bundle Widget",
+        "add": "Aggiungi Bundle Widget",
+        "delete": "Cancella bundle widget",
+        "title": "Titolo",
+        "title-required": "Titolo obbligatorio.",
+        "add-widgets-bundle-text": "Aggiungi nuovo bundle widget",
+        "no-widgets-bundles-text": "Nessun bundle widget trovato",
+        "empty": "Bundle widget vuoto",
+        "details": "Dettagli",
+        "widgets-bundle-details": "Dettagli bundle widget",
+        "delete-widgets-bundle-title": "Sei sicuro di voler eliminare il bundle widget '{{widgetsBundleTitle}}'?",
+        "delete-widgets-bundle-text": "Attenzione, dopo la conferma il bundle widget e tutti i suoi dati non saranno più recuperabili.",
+        "delete-widgets-bundles-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 bundle widget} other {# bundle widget} }?",
+        "delete-widgets-bundles-action-title": "Elimina { count, plural, 1 {1 bundle widget} other {# bundle widget} }",
+        "delete-widgets-bundles-text": "Attenzione, dopo la conferma tutti i bundle widget selezionati saranno rimossi e tutti i loro dati non saranno più recuperabili.",
+        "no-widgets-bundles-matching": "Nessun bundle widget corrispondente a  '{{widgetsBundle}}' è stato trovato.",
+        "widgets-bundle-required": "Bundle widget obbligatorio.",
+        "system": "Sistema",
+        "import": "Importa bundle widget",
+        "export": "Esporta bundle widget",
+        "export-failed-error": "Impossibile esportare bundle widget: {{error}}",
+        "create-new-widgets-bundle": "Crea nuovo bundle widget",
+        "widgets-bundle-file": "File bundle widget",
+        "invalid-widgets-bundle-file-error": "Impossibile importare bundle widget: struttura dati non valida."
+    },
+    "widget-config": {
+        "data": "Dati",
+        "settings": "Impostazioni",
+        "advanced": "Avanzate",
+        "title": "Titolo",
+        "general-settings": "Impostazioni generali",
+        "display-title": "Mostra titolo",
+        "drop-shadow": "Drop shadow",
+        "enable-fullscreen": "Abilita fullscreen",
+        "background-color": "Colore sfondo",
+        "text-color": "Colore testo",
+        "padding": "Padding",
+        "margin": "Margin",
+        "widget-style": "Stile Widget",
+        "title-style": "Stile titolo",
+        "mobile-mode-settings": "Impostazioni modalità mobile",
+        "order": "Ordinamento",
+        "height": "Altezza",
+        "units": "Simbolo speciale da mostrare vicino al valore",
+        "decimals": "Numero di cifre decimali",
+        "timewindow": "Timewindow",
+        "use-dashboard-timewindow": "Use dashboard timewindow",
+        "display-legend": "Mostra legenda",
+        "datasources": "Sorgenti dei dati",
+        "maximum-datasources": "Massimo { count, plural, 1 {1 sorgente dati consentita.} other {# sorgenti dati consentite} }",
+        "datasource-type": "Tipo",
+        "datasource-parameters": "Parametri",
+        "remove-datasource": "Rimuovi sorgente dati",
+        "add-datasource": "Aggiungi sorgente dati",
+        "target-device": "Dispositivo Target",
+        "alarm-source": "Sorgente Allarme",
+        "actions": "Azioni",
+        "action": "Azione",
+        "add-action": "Aggiungi azione",
+        "search-actions": "Ricerca azioni",
+        "action-source": "Sorgente azione",
+        "action-source-required": "Sorgente azione obbligatoria.",
+        "action-name": "Nome",
+        "action-name-required": "Nome azione obbligatorio.",
+        "action-name-not-unique": "Un'altra azione con lo stesso nome è già presente.<br/>Il nome di una azione dovrebbe essere univoco all'interno della stessa sorgente.",
+        "action-icon": "Icona",
+        "action-type": "Tipo",
+        "action-type-required": "Tipo azione obbligatorio.",
+        "edit-action": "Modifica azione",
+        "delete-action": "Cancella azione",
+        "delete-action-title": "Cancella azione del widget",
+        "delete-action-text": "Sei sicuro di voler cancellare l'azione del widget '{{actionName}}'?"
+    },
+    "widget-type": {
+        "import": "Importa un tipo di widget",
+        "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", 
+        "invalid-widget-type-file-error": "Impossibile importare un tipo di widget: struttura dati del widget non valida."
+    },
+    "icon": {
+        "icon": "Icona",
+        "select-icon": "Seleziona icona",
+        "material-icons": "Icone Material",
+        "show-all": "Mostra tutte le icone"
+    },
+    "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"
+        }
+    },
+    "language": {
+        "language": "Lingua",
+        "locales": {
+            "zh_CN": "Cinese",
+            "ko_KR": "Coreano",
+            "en_US": "Inglese",
+            "it_IT": "Italiano",
+            "ru_RU": "Russo",
+            "es_ES": "Spagnolo"
+        }
+    }
+}
diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js
index 11feb00..9fb28f5 100644
--- a/ui/src/app/locale/translate-handler.js
+++ b/ui/src/app/locale/translate-handler.js
@@ -13,8 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+ export default angular.module('thingsboard.locale', [])
+                       .factory('tbMissingTranslationHandler', ThingsboardMissingTranslateHandler)
+                       .name;
+
 /*@ngInject*/
-export default function ThingsboardMissingTranslateHandler($log, types) {
+function ThingsboardMissingTranslateHandler($log, types) {
 
     return function (translationId) {
         if (translationId && !translationId.startsWith(types.translate.customTranslationsPrefix)) {
diff --git a/ui/src/app/profile/profile.controller.js b/ui/src/app/profile/profile.controller.js
index d17a65a..b947bde 100644
--- a/ui/src/app/profile/profile.controller.js
+++ b/ui/src/app/profile/profile.controller.js
@@ -24,16 +24,9 @@ export default function ProfileController(userService, $scope, $document, $mdDia
     var vm = this;
 
     vm.profileUser = {};
-
     vm.save = save;
     vm.changePassword = changePassword;
-    vm.languageList = {
-        en_US: {value : "en_US", name: "language.en_US"}, 
-        ko_KR: {value : "ko_KR", name: "language.ko_KR"},
-        zh_CN: {value : "zh_CN", name: "language.zh_CN"},
-        ru_RU: {value : "ru_RU", name: "language.ru_RU"},
-        es_ES: {value : "es_ES", name: "language.es_ES"},
-    };
+    vm.languageList = SUPPORTED_LANGS; //eslint-disable-line
 
     loadProfile();
 
diff --git a/ui/src/app/profile/profile.tpl.html b/ui/src/app/profile/profile.tpl.html
index a0358c1..26d9925 100644
--- a/ui/src/app/profile/profile.tpl.html
+++ b/ui/src/app/profile/profile.tpl.html
@@ -43,8 +43,8 @@
                     <md-input-container class="md-block">
                         <label translate>language.language</label>
                         <md-select name="language" ng-model="vm.profileUser.additionalInfo.lang">
-                            <md-option ng-repeat="lang in vm.languageList" ng-value="lang.value">
-                                {{lang.name | translate}}
+                            <md-option ng-repeat="lang in vm.languageList" ng-value="lang">
+                                {{ lang ? ('language.locales.' + lang | translate) : ''}}
                             </md-option>
                         </md-select>
                     </md-input-container>
diff --git a/ui/src/app/rulechain/add-link.tpl.html b/ui/src/app/rulechain/add-link.tpl.html
index 0a21104..c5855b7 100644
--- a/ui/src/app/rulechain/add-link.tpl.html
+++ b/ui/src/app/rulechain/add-link.tpl.html
@@ -31,7 +31,7 @@
         <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
         <md-dialog-content>
             <div class="md-dialog-content">
-                <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
+                <tb-rule-node-link ng-model="vm.link" allowed-labels="vm.labels" is-edit="true" allow-custom="vm.allowCustomLabels"></tb-rule-node-link>
             </div>
         </md-dialog-content>
         <md-dialog-actions layout="row">
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
index 7740dd0..1ed3680 100644
--- a/ui/src/app/rulechain/index.js
+++ b/ui/src/app/rulechain/index.js
@@ -23,6 +23,7 @@ import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive'
 import RuleNodeConfigDirective from './rulenode-config.directive';
 import RuleNodeDirective from './rulenode.directive';
 import LinkDirective from './link.directive';
+import MessageTypeAutocompleteDirective from './message-type-autocomplete.directive';
 import NodeScriptTest from './script/node-script-test.service';
 
 export default angular.module('thingsboard.ruleChain', [])
@@ -37,5 +38,6 @@ export default angular.module('thingsboard.ruleChain', [])
     .directive('tbRuleNodeConfig', RuleNodeConfigDirective)
     .directive('tbRuleNode', RuleNodeDirective)
     .directive('tbRuleNodeLink', LinkDirective)
+    .directive('tbMessageTypeAutocomplete', MessageTypeAutocompleteDirective)
     .factory('ruleNodeScriptTest', NodeScriptTest)
     .name;
diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js
index b3565a3..40c7a61 100644
--- a/ui/src/app/rulechain/link.directive.js
+++ b/ui/src/app/rulechain/link.directive.js
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import './link.scss';
+
 /* eslint-disable import/no-unresolved, import/default */
 
 import linkFieldsetTemplate from './link-fieldset.tpl.html';
@@ -22,50 +24,109 @@ import linkFieldsetTemplate from './link-fieldset.tpl.html';
 
 /*@ngInject*/
 export default function LinkDirective($compile, $templateCache, $filter) {
-    var linker = function (scope, element) {
+    var linker = function (scope, element, attrs, ngModelCtrl) {
         var template = $templateCache.get(linkFieldsetTemplate);
         element.html(template);
 
         scope.selectedLabel = null;
+        scope.labelSearchText = null;
+
+        scope.ngModelCtrl = ngModelCtrl;
+
+        var labelsList = [];
+
+        scope.transformLinkLabelChip = function (chip) {
+            var res = $filter('filter')(labelsList, {name: chip}, true);
+            var result;
+            if (res && res.length) {
+                result = angular.copy(res[0]);
+            } else {
+                result = {
+                    name: chip,
+                    value: chip
+                };
+            }
+            return result;
+        };
+
+        scope.labelsSearch = function (searchText) {
+            var labels = searchText ? $filter('filter')(labelsList, {name: searchText}) : labelsList;
+            return labels.map((label) => label.name);
+        };
+
+        scope.createLinkLabel = function (event, chipsId) {
+            var chipsChild = angular.element(chipsId, element)[0].firstElementChild;
+            var el = angular.element(chipsChild);
+            var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer();
+            event.preventDefault();
+            event.stopPropagation();
+            el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim());
+            el.scope().$mdChipsCtrl.resetChipBuffer();
+        };
 
-        scope.$watch('link', function() {
-            scope.selectedLabel = null;
-             if (scope.link && scope.labels) {
-                 if (scope.link.label) {
-                     var result = $filter('filter')(scope.labels, {name: scope.link.label});
-                     if (result && result.length) {
-                         scope.selectedLabel = result[0];
-                     } else {
-                         result = $filter('filter')(scope.labels, {custom: true});
-                         if (result && result.length && result[0].custom) {
-                             scope.selectedLabel = result[0];
-                         }
-                     }
-                 }
-             }
-        });
-
-        scope.selectedLabelChanged = function() {
-            if (scope.link && scope.selectedLabel) {
-                if (!scope.selectedLabel.custom) {
-                    scope.link.label = scope.selectedLabel.name;
-                } else {
-                    scope.link.label = "";
+
+        ngModelCtrl.$render = function () {
+            labelsList.length = 0;
+            for (var label in scope.allowedLabels) {
+                var linkLabel = {
+                    name: scope.allowedLabels[label].name,
+                    value: scope.allowedLabels[label].value
+                };
+                labelsList.push(linkLabel);
+            }
+
+            var link = ngModelCtrl.$viewValue;
+            var labels = [];
+            if (link && link.labels) {
+                for (var i = 0; i < link.labels.length; i++) {
+                    label = link.labels[i];
+                    if (scope.allowedLabels[label]) {
+                        labels.push(angular.copy(scope.allowedLabels[label]));
+                    } else {
+                        labels.push({
+                            name: label,
+                            value: label
+                        });
+                    }
                 }
             }
+            scope.labels = labels;
+            scope.$watch('labels', function (newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    updateLabels();
+                }
+            }, true);
         };
 
+        function updateLabels() {
+            if (ngModelCtrl.$viewValue) {
+                var labels = [];
+                for (var i = 0; i < scope.labels.length; i++) {
+                    labels.push(scope.labels[i].value);
+                }
+                ngModelCtrl.$viewValue.labels = labels;
+                ngModelCtrl.$viewValue.label = labels.join(' / ');
+                updateValidity();
+            }
+        }
+
+        function updateValidity() {
+            var valid = ngModelCtrl.$viewValue.labels &&
+            ngModelCtrl.$viewValue.labels.length ? true : false;
+            ngModelCtrl.$setValidity('linkLabels', valid);
+        }
+
         $compile(element.contents())(scope);
     }
     return {
         restrict: "E",
+        require: "^ngModel",
         link: linker,
         scope: {
-            link: '=',
-            labels: '=',
+            allowedLabels: '=',
+            allowCustom: '=',
             isEdit: '=',
-            isReadOnly: '=',
-            theForm: '='
+            isReadOnly: '='
         }
     };
 }
diff --git a/ui/src/app/rulechain/link.scss b/ui/src/app/rulechain/link.scss
new file mode 100644
index 0000000..3e86619
--- /dev/null
+++ b/ui/src/app/rulechain/link.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-link-label-autocomplete {
+  .tb-not-found {
+    display: block;
+    line-height: 1.5;
+    height: 48px;
+    .tb-no-entries {
+      line-height: 48px;
+    }
+  }
+  li {
+    height: auto !important;
+    white-space: normal !important;
+  }
+}
diff --git a/ui/src/app/rulechain/link-fieldset.tpl.html b/ui/src/app/rulechain/link-fieldset.tpl.html
index 13ec6c3..e74542f 100644
--- a/ui/src/app/rulechain/link-fieldset.tpl.html
+++ b/ui/src/app/rulechain/link-fieldset.tpl.html
@@ -17,23 +17,44 @@
 -->
 <md-content class="md-padding tb-link" layout="column">
     <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
-        <md-input-container class="md-block">
-            <label translate>rulenode.link-label</label>
-            <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
-                <md-option ng-repeat="label in labels" ng-value="label">
-                    {{label.name}}
-                </md-option>
-            </md-select>
-            <div ng-messages="theForm.linkLabel.$error">
-                <div translate ng-message="required">rulenode.link-label-required</div>
-            </div>
-        </md-input-container>
-        <md-input-container ng-if="selectedLabel.custom" class="md-block">
-            <label translate>rulenode.link-label</label>
-            <input required name="customLinkLabel" ng-model="link.label">
-            <div ng-messages="theForm.customLinkLabel.$error">
-                <div translate ng-message="required">rulenode.custom-link-label-required</div>
-            </div>
-        </md-input-container>
+        <label translate class="tb-title no-padding" ng-class="{'tb-required': required}">rulenode.link-labels</label>
+        <md-chips id="link_label_chips"
+                  ng-required="true"
+                  readonly="$root.loading || !isEdit || isReadOnly"
+                  ng-model="labels" md-autocomplete-snap
+                  md-transform-chip="transformLinkLabelChip($chip)"
+                  md-require-match="!allowCustom">
+            <md-autocomplete
+                    id="link_label"
+                    md-no-cache="true"
+                    md-selected-item="selectedLabel"
+                    md-search-text="labelSearchText"
+                    md-items="item in labelsSearch(labelSearchText)"
+                    md-item-text="item.name"
+                    md-min-length="0"
+                    placeholder="{{'rulenode.link-label' | translate }}"
+                    md-menu-class="tb-link-label-autocomplete">
+                <span md-highlight-text="labelSearchText" md-highlight-flags="^i">{{item}}</span>
+                <md-not-found>
+                    <div class="tb-not-found">
+                        <div class="tb-no-entries" ng-if="!labelSearchText || !labelSearchText.length">
+                            <span translate>rulenode.no-link-labels-found</span>
+                        </div>
+                        <div ng-if="labelSearchText && labelSearchText.length">
+                            <span translate translate-values='{ label: "{{labelSearchText | truncate:true:6:&apos;...&apos;}}" }'>rulenode.no-link-label-matching</span>
+                            <span ng-if="allowCustom">
+                                <a translate ng-click="createLinkLabel($event, '#link_label_chips')">rulenode.create-new-link-label</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="linkLabels" class="tb-error-message">rulenode.link-labels-required</div>
+        </div>
     </fieldset>
 </md-content>
diff --git a/ui/src/app/rulechain/message-type-autocomplete.directive.js b/ui/src/app/rulechain/message-type-autocomplete.directive.js
new file mode 100644
index 0000000..afa5e35
--- /dev/null
+++ b/ui/src/app/rulechain/message-type-autocomplete.directive.js
@@ -0,0 +1,100 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './message-type-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import messageTypeAutocompleteTemplate from './message-type-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function MessageTypeAutocomplete($compile, $templateCache, $q, $filter, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(messageTypeAutocompleteTemplate);
+        element.html(template);
+
+        var messageTypeList = [];
+        for (var t in types.messageType) {
+            var type = types.messageType[t];
+            messageTypeList.push(type);
+        }
+
+        scope.messageType = null;
+        scope.messageTypeSearchText = '';
+
+        scope.fetchMessageTypes = function(searchText) {
+            var deferred = $q.defer();
+            var result = $filter('filter')(messageTypeList, {'name': searchText});
+            if (result && result.length) {
+                deferred.resolve(result);
+            } else {
+                deferred.resolve([{name: searchText, value: searchText}]);
+            }
+            return deferred.promise;
+        };
+
+        scope.messageTypeSearchTextChanged = function() {
+        };
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                var value = null;
+                if (scope.messageType) {
+                    value = scope.messageType.value;
+                }
+                ngModelCtrl.$setViewValue(value);
+            }
+        };
+
+        ngModelCtrl.$render = function () {
+            var value = ngModelCtrl.$viewValue;
+            if (value) {
+                var result = $filter('filter')(messageTypeList, {'value': value}, true);
+                if (result && result.length) {
+                    scope.messageType = result[0];
+                } else {
+                    scope.messageType = {
+                        name: value,
+                        value: value
+                    };
+                }
+            } else {
+                scope.messageType = null;
+            }
+        };
+
+        scope.$watch('messageType', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            disabled:'=ngDisabled',
+            required:'=ngRequired'
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/message-type-autocomplete.scss b/ui/src/app/rulechain/message-type-autocomplete.scss
new file mode 100644
index 0000000..4cad9cb
--- /dev/null
+++ b/ui/src/app/rulechain/message-type-autocomplete.scss
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-message-type-autocomplete {
+  .tb-message-type-item {
+    display: block;
+    height: 48px;
+  }
+  li {
+    height: auto !important;
+    white-space: normal !important;
+  }
+}
diff --git a/ui/src/app/rulechain/message-type-autocomplete.tpl.html b/ui/src/app/rulechain/message-type-autocomplete.tpl.html
new file mode 100644
index 0000000..bb3e71f
--- /dev/null
+++ b/ui/src/app/rulechain/message-type-autocomplete.tpl.html
@@ -0,0 +1,41 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-autocomplete ng-required="required"
+                 ng-disabled="disabled"
+                 md-no-cache="true"
+                 md-input-name="messageType"
+                 ng-model="messageType"
+                 md-selected-item="messageType"
+                 md-search-text="messageTypeSearchText"
+                 md-search-text-change="messageTypeSearchTextChanged()"
+                 md-items="item in fetchMessageTypes(messageTypeSearchText)"
+                 md-item-text="item.name"
+                 md-min-length="0"
+                 placeholder="{{ 'rulenode.select-message-type' | translate }}"
+                 md-floating-label="{{ 'rulenode.message-type' | translate }}"
+                 md-select-on-match="false"
+                 md-menu-class="tb-message-type-autocomplete">
+    <md-item-template>
+        <div class="tb-message-type-item">
+            <span md-highlight-text="messageTypeSearchText" md-highlight-flags="^i">{{item.name}}</span>
+        </div>
+    </md-item-template>
+    <div ng-messages="theForm.messageType.$error">
+        <div translate ng-message="required">rulenode.message-type-required</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
index dfd1a97..a38f8a8 100644
--- a/ui/src/app/rulechain/rulechain.controller.js
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -669,11 +669,15 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
                 }
             } else {
                 if (edge.label) {
+                    if (!edge.labels) {
+                        edge.labels = edge.label.split(' / ');
+                    }
                     deferred.resolve(edge);
                 } else {
                     var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+                    var allowCustomLabels = ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
                     vm.enableHotKeys = false;
-                    addRuleNodeLink(event, edge, labels).then(
+                    addRuleNodeLink(event, edge, labels, allowCustomLabels).then(
                         (link) => {
                             deferred.resolve(link);
                             vm.enableHotKeys = true;
@@ -713,6 +717,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
             vm.isEditingRuleNode = false;
             vm.editingRuleNode = null;
             vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+            vm.editingRuleNodeAllowCustomLabels = ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
             vm.isEditingRuleNodeLink = true;
             vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
             vm.editingRuleNodeLink = angular.copy(edge);
@@ -744,7 +749,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
                     isInputSource: isInputSource,
                     fromIndex: fromIndex,
                     toIndex: toIndex,
-                    label: edge.label
+                    label: edge.label,
+                    labels: edge.labels
                 };
                 connections.push(connection);
             }
@@ -816,7 +822,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
                         var edge = {
                             source: source,
                             destination: destination,
-                            label: connection.label
+                            label: connection.label,
+                            labels: connection.labels
                         };
                         vm.ruleChainModel.edges.push(edge);
                         vm.modelservice.edges.select(edge);
@@ -1024,6 +1031,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
         }
 
         if (vm.ruleChainMetaData.connections) {
+            var edgeMap = {};
             for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
                 var connection = vm.ruleChainMetaData.connections[i];
                 var sourceNode = nodes[connection.fromIndex];
@@ -1032,12 +1040,23 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
                     var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
                     var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
                     if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
-                        edge = {
-                            source: sourceConnectors[0].id,
-                            destination: destConnectors[0].id,
-                            label: connection.type
-                        };
-                        vm.ruleChainModel.edges.push(edge);
+                        var sourceId = sourceConnectors[0].id;
+                        var destId = destConnectors[0].id;
+                        var edgeKey = sourceId + '_' + destId;
+                        edge = edgeMap[edgeKey];
+                        if (!edge) {
+                            edge = {
+                                source: sourceId,
+                                destination: destId,
+                                label: connection.type,
+                                labels: [connection.type]
+                            };
+                            edgeMap[edgeKey] = edge;
+                            vm.ruleChainModel.edges.push(edge);
+                        } else {
+                            edge.label += ' / ' +connection.type;
+                            edge.labels.push(connection.type);
+                        }
                     }
                 }
             }
@@ -1045,6 +1064,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
 
         if (vm.ruleChainMetaData.ruleChainConnections) {
             var ruleChainNodesMap = {};
+            var ruleChainEdgeMap = {};
             for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
                 var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
                 var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
@@ -1081,12 +1101,23 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
                     if (sourceNode) {
                         connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
                         if (connectors && connectors.length) {
-                            var ruleChainEdge = {
-                                source: connectors[0].id,
-                                destination: ruleChainNode.connectors[0].id,
-                                label: ruleChainConnection.type
-                            };
-                            vm.ruleChainModel.edges.push(ruleChainEdge);
+                            sourceId = connectors[0].id;
+                            destId = ruleChainNode.connectors[0].id;
+                            edgeKey = sourceId + '_' + destId;
+                            var ruleChainEdge = ruleChainEdgeMap[edgeKey];
+                            if (!ruleChainEdge) {
+                                ruleChainEdge = {
+                                    source: sourceId,
+                                    destination: destId,
+                                    label: ruleChainConnection.type,
+                                    labels: [ruleChainConnection.type]
+                                };
+                                ruleChainEdgeMap[edgeKey] = ruleChainEdge;
+                                vm.ruleChainModel.edges.push(ruleChainEdge);
+                            } else {
+                                ruleChainEdge.label += ' / ' +ruleChainConnection.type;
+                                ruleChainEdge.labels.push(ruleChainConnection.type);
+                            }
                         }
                     }
                 }
@@ -1199,8 +1230,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
                             var ruleChainConnection = {
                                 fromIndex: fromIndex,
                                 targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
-                                additionalInfo: destNode.additionalInfo,
-                                type: edge.label
+                                additionalInfo: destNode.additionalInfo
                             };
                             if (!ruleChainConnection.additionalInfo) {
                                 ruleChainConnection.additionalInfo = {};
@@ -1208,15 +1238,22 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
                             ruleChainConnection.additionalInfo.layoutX = Math.round(destNode.x);
                             ruleChainConnection.additionalInfo.layoutY = Math.round(destNode.y);
                             ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
-                            ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+                            for (var rcIndex=0;rcIndex<edge.labels.length;rcIndex++) {
+                                var newRuleChainConnection = angular.copy(ruleChainConnection);
+                                newRuleChainConnection.type = edge.labels[rcIndex];
+                                ruleChainMetaData.ruleChainConnections.push(newRuleChainConnection);
+                            }
                         } else {
                             var toIndex = nodes.indexOf(destNode);
                             var nodeConnection = {
                                 fromIndex: fromIndex,
-                                toIndex: toIndex,
-                                type: edge.label
+                                toIndex: toIndex
                             };
-                            ruleChainMetaData.connections.push(nodeConnection);
+                            for (var cIndex=0;cIndex<edge.labels.length;cIndex++) {
+                                var newNodeConnection = angular.copy(nodeConnection);
+                                newNodeConnection.type = edge.labels[cIndex];
+                                ruleChainMetaData.connections.push(newNodeConnection);
+                            }
                         }
                     }
                 }
@@ -1285,13 +1322,13 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
         });
     }
 
-    function addRuleNodeLink($event, link, labels) {
+    function addRuleNodeLink($event, link, labels, allowCustomLabels) {
         return $mdDialog.show({
             controller: 'AddRuleNodeLinkController',
             controllerAs: 'vm',
             templateUrl: addRuleNodeLinkTemplate,
             parent: angular.element($document[0].body),
-            locals: {link: link, labels: labels},
+            locals: {link: link, labels: labels, allowCustomLabels: allowCustomLabels},
             fullscreen: true,
             targetEvent: $event
         });
@@ -1335,13 +1372,14 @@ export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, 
 }
 
 /*@ngInject*/
-export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
+export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, allowCustomLabels, helpLinks) {
 
     var vm = this;
 
     vm.helpLinks = helpLinks;
     vm.link = link;
     vm.labels = labels;
+    vm.allowCustomLabels = allowCustomLabels;
 
     vm.add = add;
     vm.cancel = cancel;
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
index c51a955..5999b7e 100644
--- a/ui/src/app/rulechain/rulechain.scss
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -170,6 +170,9 @@
   &.tb-rule-chain-type {
     background-color: #d6c4f1;
   }
+  &.tb-unknown-type {
+    background-color: #f16c29;
+  }
 }
 
 .tb-rule-node {
@@ -202,6 +205,7 @@
     background-color: #a3eaa9;
     user-select: none;
   }
+
   md-icon {
     font-size: 20px;
     width: 20px;
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
index a84df90..9c77ae4 100644
--- a/ui/src/app/rulechain/rulechain.tpl.html
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -207,11 +207,11 @@
             </details-buttons>
             <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
                 <tb-rule-node-link
-                        link="vm.editingRuleNodeLink"
-                        labels="vm.editingRuleNodeLinkLabels"
+                        ng-model="vm.editingRuleNodeLink"
+                        allowed-labels="vm.editingRuleNodeLinkLabels"
+                        allow-custom="vm.editingRuleNodeAllowCustomLabels"
                         is-edit="true"
-                        is-read-only="false"
-                        the-form="vm.ruleNodeLinkForm">
+                        is-read-only="false">
                 </tb-rule-node-link>
             </form>
         </tb-details-sidenav>
diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html
index 0ce57e8..e5fb9c3 100644
--- a/ui/src/app/rulechain/script/node-script-test.tpl.html
+++ b/ui/src/app/rulechain/script/node-script-test.tpl.html
@@ -38,13 +38,12 @@
                             <ng-form name="payloadForm">
                                 <div layout="column" style="height: 100%;">
                                     <div layout="row">
-                                        <md-input-container class="md-block" style="margin-bottom: 0px; min-width: 300px;">
-                                            <label translate>rulenode.message-type</label>
-                                            <input required name="msgType" ng-model="vm.inputParams.msgType">
-                                            <div ng-messages="payloadForm.msgType.$error">
-                                                <div translate ng-message="required">rulenode.message-type-required</div>
-                                            </div>
-                                        </md-input-container>
+                                        <tb-message-type-autocomplete
+                                                style="margin-bottom: 0px; min-width: 300px;"
+                                                ng-required="true"
+                                                ng-model="vm.inputParams.msgType"
+                                                the-form="payloadForm">
+                                        </tb-message-type-autocomplete>
                                     </div>
                                     <tb-json-content flex
                                                      ng-model="vm.inputParams.msg"
@@ -99,14 +98,14 @@
                                              validate-content="false"
                                              ng-readonly="true"
                                              fill-height="true">
-                            </tb-json-content>
+                            </tb-json-content>generateReport
                         </div>
                     </div>
                 </div>
             </div>
         </md-dialog-content>
         <md-dialog-actions layout="row">
-            <md-button ng-disabled="$root.loading" ng-click="vm.test()" class="md-raised md-primary">
+            <md-button ng-disabled="$root.loading || theForm.$invalid" ng-click="vm.test()" class="md-raised md-primary">
                 {{ 'rulenode.test' | translate }}
             </md-button>
             <span flex></span>
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index a9fe348..09ed9c5 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -214,10 +214,19 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainServ
                 var node = ruleNodes.nodes[i];
                 var component = ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
                 if (component) {
+                    var icon = types.ruleNodeType[component.type].icon;
+                    var iconUrl = null;
+                    if (component.configurationDescriptor.nodeDefinition.icon) {
+                        icon = component.configurationDescriptor.nodeDefinition.icon;
+                    }
+                    if (component.configurationDescriptor.nodeDefinition.iconUrl) {
+                        iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
+                    }
                     delete node.componentClazz;
                     node.component = component;
                     node.nodeClass = types.ruleNodeType[component.type].nodeClass;
-                    node.icon = types.ruleNodeType[component.type].icon;
+                    node.icon = icon;
+                    node.iconUrl = iconUrl;
                     node.connectors = [];
                     node.x = Math.round(node.x + deltaX);
                     node.y = Math.round(node.y + deltaY);
diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js
index 0a5bbce..0696a7b 100644
--- a/ui/src/app/widget/lib/alarms-table-widget.js
+++ b/ui/src/app/widget/lib/alarms-table-widget.js
@@ -45,7 +45,7 @@ function AlarmsTableWidget() {
 }
 
 /*@ngInject*/
-function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, alarmService, utils, types) {
+function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDialog, $document, $translate, $q, $timeout, alarmService, utils, types) {
     var vm = this;
 
     vm.stylesInfo = {};
@@ -266,6 +266,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
     function enterFilterMode () {
         vm.query.search = '';
         vm.ctx.hideTitlePanel = true;
+        $timeout(()=>{
+            angular.element(vm.ctx.$container).find('.searchInput').focus();
+        })
     }
 
     function exitFilterMode () {
diff --git a/ui/src/app/widget/lib/alarms-table-widget.tpl.html b/ui/src/app/widget/lib/alarms-table-widget.tpl.html
index 1cca18b..8480058 100644
--- a/ui/src/app/widget/lib/alarms-table-widget.tpl.html
+++ b/ui/src/app/widget/lib/alarms-table-widget.tpl.html
@@ -28,7 +28,7 @@
                 </md-button>
                 <md-input-container flex>
                     <label>&nbsp;</label>
-                    <input ng-model="vm.query.search" placeholder="{{'alarm.search' | translate}}"/>
+                    <input ng-model="vm.query.search" class="searchInput" placeholder="{{'alarm.search' | translate}}"/>
                 </md-input-container>
                 <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
                     <md-icon aria-label="Close" class="material-icons">close</md-icon>
diff --git a/ui/src/app/widget/lib/canvas-digital-gauge.js b/ui/src/app/widget/lib/canvas-digital-gauge.js
index 8e6db04..283a426 100644
--- a/ui/src/app/widget/lib/canvas-digital-gauge.js
+++ b/ui/src/app/widget/lib/canvas-digital-gauge.js
@@ -70,6 +70,10 @@ export default class TbCanvasDigitalGauge {
             (settings.title && settings.title.length > 0 ?
                 settings.title : dataKey.label) : '');
 
+        if (!this.localSettings.unitTitle && this.localSettings.showTimestamp) {
+            this.localSettings.unitTitle = ' ';
+        }
+
         this.localSettings.titleFont = {};
         var settingsTitleFont = settings.titleFont;
         if (!settingsTitleFont) {
@@ -206,6 +210,7 @@ export default class TbCanvasDigitalGauge {
                 var value = tvPair[1];
                 if(value !== this.gauge.value) {
                     this.gauge.value = value;
+                    this.gauge._value = value;
                 } else if (this.localSettings.showTimestamp && this.gauge.timestamp != timestamp) {
                     this.gauge.timestamp = timestamp;
                 }
diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js
index 0166e91..ee8b0ed 100644
--- a/ui/src/app/widget/lib/CanvasDigitalGauge.js
+++ b/ui/src/app/widget/lib/CanvasDigitalGauge.js
@@ -204,8 +204,13 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
             }
 
             var valueChanged = false;
-
-            if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) {
+            if (!this.elementValueClone.initialized || angular.isDefined(this._value) && this.elementValueClone.renderedValue !== this._value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) {
+                if (angular.isDefined(this._value)) {
+                    this.elementValueClone.renderedValue = this._value;
+                }
+                if (angular.isUndefined(this.elementValueClone.renderedValue)) {
+                    this.elementValueClone.renderedValue = options.minValue;
+                }
                 let context = this.contextValueClone;
                 // clear the cache
                 context.clearRect(x, y, w, h);
@@ -214,7 +219,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
                 context.drawImage(canvas.elementClone, x, y, w, h);
                 context.save();
 
-                drawDigitalValue(context, options, this.value);
+                drawDigitalValue(context, options, this.elementValueClone.renderedValue);
 
                 if (options.showTimestamp) {
                     drawDigitalLabel(context, options);
@@ -222,7 +227,6 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
                 }
 
                 this.elementValueClone.initialized = true;
-                this.elementValueClone.renderedValue = this.value;
 
                 valueChanged = true;
             }
diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js
index 1601e71..d0b629d 100644
--- a/ui/src/app/widget/lib/entities-table-widget.js
+++ b/ui/src/app/widget/lib/entities-table-widget.js
@@ -45,7 +45,7 @@ function EntitiesTableWidget() {
 }
 
 /*@ngInject*/
-function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $translate, utils, types) {
+function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $translate, $timeout, utils, types) {
     var vm = this;
 
     vm.stylesInfo = {};
@@ -254,6 +254,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
     function enterFilterMode () {
         vm.query.search = '';
         vm.ctx.hideTitlePanel = true;
+        $timeout(()=>{
+            angular.element(vm.ctx.$container).find('.searchInput').focus();
+        })
     }
 
     function exitFilterMode () {
diff --git a/ui/src/app/widget/lib/entities-table-widget.tpl.html b/ui/src/app/widget/lib/entities-table-widget.tpl.html
index 97a8d69..474d536 100644
--- a/ui/src/app/widget/lib/entities-table-widget.tpl.html
+++ b/ui/src/app/widget/lib/entities-table-widget.tpl.html
@@ -27,7 +27,7 @@
                 </md-button>
                 <md-input-container flex>
                     <label>&nbsp;</label>
-                    <input ng-model="vm.query.search" placeholder="{{'entity.search' | translate}}"/>
+                    <input ng-model="vm.query.search" class="searchInput" placeholder="{{'entity.search' | translate}}"/>
                 </md-input-container>
                 <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
                     <md-icon aria-label="Close" class="material-icons">close</md-icon>
diff --git a/ui/src/app/widget/lib/timeseries-table-widget.js b/ui/src/app/widget/lib/timeseries-table-widget.js
index 684bbde..bca7956 100644
--- a/ui/src/app/widget/lib/timeseries-table-widget.js
+++ b/ui/src/app/widget/lib/timeseries-table-widget.js
@@ -44,7 +44,7 @@ function TimeseriesTableWidget() {
 }
 
 /*@ngInject*/
-function TimeseriesTableWidgetController($element, $scope, $filter) {
+function TimeseriesTableWidgetController($element, $scope, $filter, $timeout) {
     var vm = this;
     let dateFormatFilter = 'yyyy-MM-dd HH:mm:ss';
 
@@ -62,6 +62,9 @@ function TimeseriesTableWidgetController($element, $scope, $filter) {
     function enterFilterMode () {
         vm.query.search = '';
         vm.ctx.hideTitlePanel = true;
+        $timeout(()=>{
+            angular.element(vm.ctx.$container).find('.searchInput').focus();
+        })
     }
 
     function exitFilterMode () {
diff --git a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html
index eb9b8ca..08c4c9e 100644
--- a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html
+++ b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html
@@ -27,7 +27,7 @@
                 </md-button>
                 <md-input-container flex>
                     <label>&nbsp;</label>
-                    <input ng-model="vm.query.search" placeholder="{{'widget.search-data' | translate}}" md-autofocus/>
+                    <input ng-model="vm.query.search" class="searchInput" placeholder="{{'widget.search-data' | translate}}" md-autofocus/>
                 </md-input-container>
                 <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
                     <md-icon aria-label="Close" class="material-icons">close</md-icon>
diff --git a/ui/src/app/widget/widget-editor.controller.js b/ui/src/app/widget/widget-editor.controller.js
index 6f0ad87..089d64c 100644
--- a/ui/src/app/widget/widget-editor.controller.js
+++ b/ui/src/app/widget/widget-editor.controller.js
@@ -20,12 +20,11 @@ import 'brace/mode/javascript';
 import 'brace/mode/html';
 import 'brace/mode/css';
 import 'brace/mode/json';
-import 'ace-builds/src-min-noconflict/ace';
-import 'ace-builds/src-min-noconflict/snippets/javascript';
-import 'ace-builds/src-min-noconflict/snippets/text';
-import 'ace-builds/src-min-noconflict/snippets/html';
-import 'ace-builds/src-min-noconflict/snippets/css';
-import 'ace-builds/src-min-noconflict/snippets/json';
+import 'brace/snippets/javascript';
+import 'brace/snippets/text';
+import 'brace/snippets/html';
+import 'brace/snippets/css';
+import 'brace/snippets/json';
 
 /* eslint-disable import/no-unresolved, import/default */
 
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 8fed892..2852a7b 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -16,6 +16,7 @@
 @import "~compass-sass-mixins/lib/compass";
 @import "constants";
 @import "animations";
+@import "mixins";
 @import "fonts";
 
 /***************
@@ -437,6 +438,12 @@ pre.tb-highlight {
   }
 }
 
+.tb-card-description {
+  color: rgba(0,0,0,0.54);
+  font-size: 13px;
+  @include line-clamp(2, 1.1);
+}
+
 /***********************
  * Flow
  ***********************/
diff --git a/ui/src/scss/mixins.scss b/ui/src/scss/mixins.scss
index 9e8b7df..cb66171 100644
--- a/ui/src/scss/mixins.scss
+++ b/ui/src/scss/mixins.scss
@@ -31,4 +31,29 @@
   &:-ms-input-placeholder {
     @content;
   }
-}
\ No newline at end of file
+}
+
+@mixin line-clamp($numLines: 1, $lineHeight: 1.412) {
+  overflow: hidden;
+  position: relative;
+  line-height: $lineHeight;
+  text-align: justify;
+  margin-right: -1em;
+  padding-right: 2em;
+  max-height: ($numLines*$lineHeight)+em;
+  &:before {
+    content: '...';
+    position: absolute;
+    right: 1em;
+    bottom: 0;
+  }
+  &:after {
+    content: '';
+    position: absolute;
+    right: 1em;
+    width: 1em;
+    height: 1em;
+    margin-top: 0.2em;
+    background: white;
+  }
+}
diff --git a/ui/webpack.config.dev.js b/ui/webpack.config.dev.js
index b1a1911..242ad9d 100644
--- a/ui/webpack.config.dev.js
+++ b/ui/webpack.config.dev.js
@@ -20,6 +20,17 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
 const webpack = require('webpack');
 const path = require('path');
+const dirTree = require('directory-tree');
+const jsonminify = require("jsonminify");
+
+const PUBLIC_RESOURCE_PATH = '/';
+
+var langs = [];
+dirTree('./src/app/locale/', {extensions:/\.json$/}, (item) => {
+    /* It is expected what the name of a locale file has the following format: */
+    /* 'locale.constant-LANG_CODE[_REGION_CODE].json', e.g. locale.constant-es.json or locale.constant-zh_CN.json*/
+    langs.push(item.name.slice(item.name.lastIndexOf('-') + 1, -5));
+});
 
 /* devtool: 'cheap-module-eval-source-map', */
 
@@ -32,7 +43,7 @@ module.exports = {
     ],
     output: {
         path: path.resolve(__dirname, 'target/generated-resources/public/static'),
-        publicPath: '/',
+        publicPath: PUBLIC_RESOURCE_PATH,
         filename: 'bundle.js',
     },
     plugins: [
@@ -45,7 +56,18 @@ module.exports = {
             moment: "moment"
         }),
         new CopyWebpackPlugin([
-            { from: './src/thingsboard.ico', to: 'thingsboard.ico' }
+            {
+                from: './src/thingsboard.ico',
+                to: 'thingsboard.ico'
+            },
+            {
+                from: './src/app/locale',
+                to: 'locale',
+                ignore: [ '*.js' ],
+                transform: function(content, path) {
+                    return Buffer.from(jsonminify(content.toString()));
+                }
+            }
         ]),
         new webpack.HotModuleReplacementPlugin(),
         new HtmlWebpackPlugin({
@@ -65,6 +87,8 @@ module.exports = {
             'process.env': {
                 NODE_ENV: JSON.stringify('development'),
             },
+            PUBLIC_PATH: JSON.stringify(PUBLIC_RESOURCE_PATH),
+            SUPPORTED_LANGS: JSON.stringify(langs)
         }),
     ],
     node: {
@@ -117,7 +141,7 @@ module.exports = {
                     'url?limit=8192',
                     'img?minimize'
                 ]
-            },
+            }
         ],
     },
     'html-minifier-loader': {
diff --git a/ui/webpack.config.prod.js b/ui/webpack.config.prod.js
index 150638d..a442590 100644
--- a/ui/webpack.config.prod.js
+++ b/ui/webpack.config.prod.js
@@ -18,8 +18,20 @@
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
+const CompressionPlugin = require('compression-webpack-plugin');
 const webpack = require('webpack');
 const path = require('path');
+const dirTree = require('directory-tree');
+const jsonminify = require("jsonminify");
+
+const PUBLIC_RESOURCE_PATH = '/static/';
+
+var langs = [];
+dirTree('./src/app/locale/', {extensions:/\.json$/}, (item) => {
+    /* It is expected what the name of a locale file has the following format: */
+    /* 'locale.constant-LANG_CODE[_REGION_CODE].json', e.g. locale.constant-es.json or locale.constant-zh_CN.json*/
+    langs.push(item.name.slice(item.name.lastIndexOf('-') + 1, -5));
+});
 
 module.exports = {
     devtool: 'source-map',
@@ -29,7 +41,7 @@ module.exports = {
     ],
     output: {
         path: path.resolve(__dirname, 'target/generated-resources/public/static'),
-        publicPath: '/static/',
+        publicPath: PUBLIC_RESOURCE_PATH,
         filename: 'bundle.[hash].js',
     },
     plugins: [
@@ -42,7 +54,18 @@ module.exports = {
             moment: "moment"
         }),
         new CopyWebpackPlugin([
-            {from: './src/thingsboard.ico', to: 'thingsboard.ico'}
+            {
+                from: './src/thingsboard.ico',
+                to: 'thingsboard.ico'
+            },
+            {
+                from: './src/app/locale',
+                to: 'locale',
+                ignore: [ '*.js' ],
+                transform: function(content, path) {
+                    return Buffer.from(jsonminify(content.toString()));
+                }
+            }
         ]),
         new HtmlWebpackPlugin({
             template: './src/index.html',
@@ -63,7 +86,16 @@ module.exports = {
             'process.env': {
                 NODE_ENV: JSON.stringify('production'),
             },
+            PUBLIC_PATH: PUBLIC_RESOURCE_PATH,
+            SUPPORTED_LANGS: JSON.stringify(langs)
         }),
+        new CompressionPlugin({
+            asset: "[path].gz[query]",
+            algorithm: "gzip",
+            test: /\.js$|\.css$|\.svg$|\.ttf$|\.woff$|\.woff2|\.eot$\.json$/,
+            threshold: 10240,
+            minRatio: 0.8
+        })
     ],
     node: {
         tls: "empty",
@@ -115,7 +147,7 @@ module.exports = {
                     'url?limit=8192',
                     'img?minimize'
                 ]
-            },
+            }
         ],
     },
     'html-minifier-loader': {