thingsboard-memoizeit

Merge with master

5/29/2017 6:21:24 AM

Changes

docker/.env 6(+6 -0)

docker/deploy.sh 31(+0 -31)

docker/deploy_cassandra_zookeeper.sh 31(+0 -31)

docker/tb.env 3(+2 -1)

docker/tb/Makefile 11(+11 -0)

docker/tb/tb.yaml 121(+121 -0)

docker/thingsboard-db-schema.env 5(+0 -5)

docker/thingsboard-db-schema/build_and_deploy.sh 28(+0 -28)

Details

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 ec93d29..aba4b6e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -343,7 +343,7 @@ public abstract class BaseController {
     Alarm checkAlarmId(AlarmId alarmId) throws ThingsboardException {
         try {
             validateId(alarmId, "Incorrect alarmId " + alarmId);
-            Alarm alarm = alarmService.findAlarmById(alarmId).get();
+            Alarm alarm = alarmService.findAlarmByIdAsync(alarmId).get();
             checkAlarm(alarm);
             return alarm;
         } catch (Exception e) {
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 0c1fd8b..04f1c3f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationInfo;
 import org.thingsboard.server.dao.relation.EntityRelationsQuery;
 import org.thingsboard.server.exception.ThingsboardErrorCode;
 import org.thingsboard.server.exception.ThingsboardException;
@@ -128,6 +129,21 @@ public class EntityRelationController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"fromId", "fromType"})
+    @ResponseBody
+    public List<EntityRelationInfo> findInfoByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType) throws ThingsboardException {
+        checkParameter("fromId", strFromId);
+        checkParameter("fromType", strFromType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
+        checkEntityId(entityId);
+        try {
+            return checkNotNull(relationService.findInfoByFrom(entityId).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"})
     @ResponseBody
     public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 69b1cd7..4b488af 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -19,12 +19,18 @@ server:
   address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
   # Server bind port
   port: "${HTTP_BIND_PORT:8080}"
-# Uncomment the following section to enable ssl
-#  ssl:
-#    key-store: classpath:keystore/keystore.p12
-#    key-store-password: thingsboard
-#    keyStoreType: PKCS12
-#    keyAlias: tomcat
+  # Server SSL configuration
+  ssl:
+    # Enable/disable SSL support
+    enabled: "${SSL_ENABLED:false}"
+    # Path to the key store that holds the SSL certificate
+    key-store: "${SSL_KEY_STORE:classpath:keystore/keystore.p12}"
+    # Password used to access the key store
+    key-store-password: "${SSL_KEY_STORE_PASSWORD:thingsboard}"
+    # Type of the key store
+    key-store-type: "${SSL_KEY_STORE_TYPE:PKCS12}"
+    # Alias that identifies the key in the key store
+    key-alias: "${SSL_KEY_ALIAS:tomcat}"
 
 # Zookeeper connection parameters. Used for service discovery.
 zk:
@@ -79,12 +85,18 @@ mqtt:
     leak_detector_level: "${NETTY_LEASK_DETECTOR_LVL:DISABLED}"
     boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
     worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
-# Uncomment the following lines to enable ssl for MQTT
-#  ssl:
-#    key_store: mqttserver.jks
-#    key_store_password: server_ks_password
-#    key_password: server_key_password
-#    key_store_type: JKS
+  # MQTT SSL configuration
+  ssl:
+    # Enable/disable SSL support
+    enabled: "${MQTT_SSL_ENABLED:false}"
+    # Path to the key store that holds the SSL certificate
+    key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
+    # Password used to access the key store
+    key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
+    # Password used to access the key
+    key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
+    # Type of the key store
+    key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
 
 # CoAP server parameters
 coap:
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 6691670..7bf7a08 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -21,6 +21,7 @@ import lombok.Builder;
 import lombok.Data;
 import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 
@@ -30,7 +31,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 @Data
 @Builder
 @AllArgsConstructor
-public class Alarm extends BaseData<AlarmId> {
+public class Alarm extends BaseData<AlarmId> implements HasName {
 
     private TenantId tenantId;
     private String type;
@@ -52,4 +53,8 @@ public class Alarm extends BaseData<AlarmId> {
         super(id);
     }
 
+    @Override
+    public String getName() {
+        return type;
+    }
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
index 2e20b77..dafa514 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -16,12 +16,13 @@
 package org.thingsboard.server.common.data.asset;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.SearchTextBased;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 
-public class Asset extends SearchTextBased<AssetId> {
+public class Asset extends SearchTextBased<AssetId> implements HasName {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
@@ -64,6 +65,7 @@ public class Asset extends SearchTextBased<AssetId> {
         this.customerId = customerId;
     }
 
+    @Override
     public String getName() {
         return name;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
index 71a4527..f8c1e30 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Customer extends ContactBased<CustomerId>{
+public class Customer extends ContactBased<CustomerId> implements HasName {
     
     private static final long serialVersionUID = -1599722990298929275L;
     
@@ -59,6 +59,11 @@ public class Customer extends ContactBased<CustomerId>{
         this.title = title;
     }
 
+    @Override
+    public String getName() {
+        return title;
+    }
+
     public JsonNode getAdditionalInfo() {
         return additionalInfo;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
index d88df16..c4daf9d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
@@ -19,7 +19,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DashboardId;
 import org.thingsboard.server.common.data.id.TenantId;
 
-public class DashboardInfo extends SearchTextBased<DashboardId> {
+public class DashboardInfo extends SearchTextBased<DashboardId> implements HasName {
 
     private TenantId tenantId;
     private CustomerId customerId;
@@ -65,6 +65,11 @@ public class DashboardInfo extends SearchTextBased<DashboardId> {
     }
 
     @Override
+    public String getName() {
+        return title;
+    }
+
+    @Override
     public String getSearchText() {
         return title;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
index 92e8655..7d3d4f5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Device extends SearchTextBased<DeviceId> {
+public class Device extends SearchTextBased<DeviceId> implements HasName {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
@@ -64,6 +64,7 @@ public class Device extends SearchTextBased<DeviceId> {
         this.customerId = customerId;
     }
 
+    @Override
     public String getName() {
         return name;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasName.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasName.java
new file mode 100644
index 0000000..f431989
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasName.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016-2017 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.data;
+
+public interface HasName {
+
+    String getName();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
index 5019cd1..e5eb149 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
@@ -15,13 +15,14 @@
  */
 package org.thingsboard.server.common.data.plugin;
 
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.SearchTextBased;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class PluginMetaData extends SearchTextBased<PluginId> {
+public class PluginMetaData extends SearchTextBased<PluginId> implements HasName {
 
     private static final long serialVersionUID = 1L;
 
@@ -75,6 +76,7 @@ public class PluginMetaData extends SearchTextBased<PluginId> {
         this.tenantId = tenantId;
     }
 
+    @Override
     public String getName() {
         return name;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
index 8fcf269..39e7d54 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
@@ -47,11 +47,11 @@ public class EntityRelation {
         this.additionalInfo = additionalInfo;
     }
 
-    public EntityRelation(EntityRelation device) {
-        this.from = device.getFrom();
-        this.to = device.getTo();
-        this.type = device.getType();
-        this.additionalInfo = device.getAdditionalInfo();
+    public EntityRelation(EntityRelation entityRelation) {
+        this.from = entityRelation.getFrom();
+        this.to = entityRelation.getTo();
+        this.type = entityRelation.getType();
+        this.additionalInfo = entityRelation.getAdditionalInfo();
     }
 
     public EntityId getFrom() {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java
new file mode 100644
index 0000000..709ad79
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2017 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.data.relation;
+
+public class EntityRelationInfo extends EntityRelation {
+
+    private static final long serialVersionUID = 2807343097519543363L;
+
+    private String toName;
+
+    public EntityRelationInfo() {
+        super();
+    }
+
+    public EntityRelationInfo(EntityRelation entityRelation) {
+        super(entityRelation);
+    }
+
+    public String getToName() {
+        return toName;
+    }
+
+    public void setToName(String toName) {
+        this.toName = toName;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+
+        EntityRelationInfo that = (EntityRelationInfo) o;
+
+        return toName != null ? toName.equals(that.toName) : that.toName == null;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (toName != null ? toName.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
index ecbc86c..8a0f847 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.rule;
 
 import lombok.Data;
 import lombok.ToString;
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.SearchTextBased;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.RuleId;
@@ -26,7 +27,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
 
 @Data
-public class RuleMetaData extends SearchTextBased<RuleId> {
+public class RuleMetaData extends SearchTextBased<RuleId> implements HasName {
 
     private static final long serialVersionUID = -5656679015122935465L;
 
@@ -66,4 +67,9 @@ public class RuleMetaData extends SearchTextBased<RuleId> {
         return name;
     }
 
+    @Override
+    public String getName() {
+        return name;
+    }
+
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
index aee1510..3a5f6bf 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
@@ -19,7 +19,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Tenant extends ContactBased<TenantId>{
+public class Tenant extends ContactBased<TenantId> implements HasName {
 
     private static final long serialVersionUID = 8057243243859922101L;
     
@@ -50,6 +50,11 @@ public class Tenant extends ContactBased<TenantId>{
         this.title = title;
     }
 
+    @Override
+    public String getName() {
+        return title;
+    }
+
     public String getRegion() {
         return region;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
index 74fa4cf..0543ef0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.security.Authority;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class User extends SearchTextBased<UserId> {
+public class User extends SearchTextBased<UserId> implements HasName {
 
     private static final long serialVersionUID = 8250339805336035966L;
 
@@ -77,6 +77,11 @@ public class User extends SearchTextBased<UserId> {
         this.email = email;
     }
 
+    @Override
+    public String getName() {
+        return email;
+    }
+
     public Authority getAuthority() {
         return authority;
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
index 3c4da18..5399d9d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
@@ -32,7 +32,7 @@ public interface AlarmService {
 
     ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long ackTs);
 
-    ListenableFuture<Alarm> findAlarmById(AlarmId alarmId);
+    ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId);
 
     ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query);
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
index a5bf27c..5b31a51 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.entity.BaseEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.model.*;
@@ -54,7 +55,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
 
 @Service
 @Slf4j
-public class BaseAlarmService extends BaseEntityService implements AlarmService {
+public class BaseAlarmService extends AbstractEntityService implements AlarmService {
 
     public static final String ALARM_RELATION_PREFIX = "ALARM_";
     public static final String ALARM_RELATION = "ALARM_ANY";
@@ -190,7 +191,7 @@ public class BaseAlarmService extends BaseEntityService implements AlarmService 
     }
 
     @Override
-    public ListenableFuture<Alarm> findAlarmById(AlarmId alarmId) {
+    public ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId) {
         log.trace("Executing findAlarmById [{}]", alarmId);
         validateId(alarmId, "Incorrect alarmId " + alarmId);
         return alarmDao.findAlarmByIdAsync(alarmId.getId());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
index fce0c5e..3a5f803 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
@@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.model.*;
 import org.thingsboard.server.dao.relation.EntitySearchDirection;
@@ -55,7 +55,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
 
 @Service
 @Slf4j
-public class BaseAssetService extends BaseEntityService implements AssetService {
+public class BaseAssetService extends AbstractEntityService implements AssetService {
 
     @Autowired
     private AssetDao assetDao;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java
index b4fbc65..7c435bf 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java
@@ -45,7 +45,6 @@ public class ServiceCacheConfiguration {
     @Value("${cache.device_credentials.time_to_live}")
     private Integer cacheDeviceCredentialsTTL;
 
-
     @Value("${zk.enabled}")
     private boolean zkEnabled;
     @Value("${zk.url}")
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
index e9f6836..ff726da 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
@@ -31,17 +31,15 @@ import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.thingsboard.server.common.data.Customer;
-import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.AssetEntity;
 import org.thingsboard.server.dao.model.CustomerEntity;
 import org.thingsboard.server.dao.model.TenantEntity;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -53,7 +51,7 @@ import org.springframework.stereotype.Service;
 import org.thingsboard.server.dao.service.Validator;
 @Service
 @Slf4j
-public class CustomerServiceImpl extends BaseEntityService implements CustomerService {
+public class CustomerServiceImpl extends AbstractEntityService implements CustomerService {
 
     private static final String PUBLIC_CUSTOMER_TITLE = "Public";
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
index b0ebbfd..f1f174e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.dashboard;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Dashboard;
 import org.thingsboard.server.common.data.DashboardInfo;
 import org.thingsboard.server.common.data.id.CustomerId;
@@ -27,8 +28,12 @@ public interface DashboardService {
     
     public Dashboard findDashboardById(DashboardId dashboardId);
 
+    public ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId);
+
     public DashboardInfo findDashboardInfoById(DashboardId dashboardId);
 
+    public ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId);
+
     public Dashboard saveDashboard(Dashboard dashboard);
     
     public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index cf554f3..fc3e176 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -17,9 +17,13 @@ package org.thingsboard.server.dao.dashboard;
 
 import static org.thingsboard.server.dao.DaoUtil.convertDataList;
 import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
 
 import java.util.List;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.thingsboard.server.common.data.Dashboard;
@@ -30,7 +34,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.model.*;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -42,7 +46,7 @@ import org.thingsboard.server.dao.service.Validator;
 
 @Service
 @Slf4j
-public class DashboardServiceImpl extends BaseEntityService implements DashboardService {
+public class DashboardServiceImpl extends AbstractEntityService implements DashboardService {
 
     @Autowired
     private DashboardDao dashboardDao;
@@ -65,6 +69,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
     }
 
     @Override
+    public ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId) {
+        log.trace("Executing findDashboardByIdAsync [{}]", dashboardId);
+        validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        ListenableFuture<DashboardEntity> dashboardEntity = dashboardDao.findByIdAsync(dashboardId.getId());
+        return Futures.transform(dashboardEntity, (Function<? super DashboardEntity, ? extends Dashboard>) input -> getData(input));
+    }
+
+    @Override
     public DashboardInfo findDashboardInfoById(DashboardId dashboardId) {
         log.trace("Executing findDashboardInfoById [{}]", dashboardId);
         Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
@@ -73,6 +85,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
     }
 
     @Override
+    public ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId) {
+        log.trace("Executing findDashboardInfoByIdAsync [{}]", dashboardId);
+        validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        ListenableFuture<DashboardInfoEntity> dashboardInfoEntity = dashboardInfoDao.findByIdAsync(dashboardId.getId());
+        return Futures.transform(dashboardInfoEntity, (Function<? super DashboardInfoEntity, ? extends DashboardInfo>) input -> getData(input));
+    }
+
+    @Override
     public Dashboard saveDashboard(Dashboard dashboard) {
         log.trace("Executing saveDashboard [{}]", dashboard);
         dashboardValidator.validate(dashboard);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
index d01f154..99585ea 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
@@ -37,7 +37,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.common.data.security.DeviceCredentialsType;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.model.CustomerEntity;
 import org.thingsboard.server.dao.model.DeviceEntity;
@@ -58,7 +58,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
 
 @Service
 @Slf4j
-public class DeviceServiceImpl extends BaseEntityService implements DeviceService {
+public class DeviceServiceImpl extends AbstractEntityService implements DeviceService {
 
     @Autowired
     private DeviceDao deviceDao;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
new file mode 100644
index 0000000..ecca491
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2017 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.dao.entity;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.dao.relation.RelationService;
+
+@Slf4j
+public abstract class AbstractEntityService {
+
+    @Autowired
+    protected RelationService relationService;
+
+    protected void deleteEntityRelations(EntityId entityId) {
+        log.trace("Executing deleteEntityRelations [{}]", entityId);
+        relationService.deleteEntityRelations(entityId);
+    }
+
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
index 3c1c528..6f9500e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
@@ -15,23 +15,102 @@
  */
 package org.thingsboard.server.dao.entity;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.dao.relation.RelationService;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.user.UserService;
 
 /**
  * Created by ashvayka on 04.05.17.
  */
+@Service
 @Slf4j
-public class BaseEntityService {
+public class BaseEntityService extends AbstractEntityService implements EntityService {
 
     @Autowired
-    protected RelationService relationService;
+    private AssetService assetService;
 
-    protected void deleteEntityRelations(EntityId entityId) {
-        log.trace("Executing deleteEntityRelations [{}]", entityId);
-        relationService.deleteEntityRelations(entityId);
+    @Autowired
+    private DeviceService deviceService;
+
+    @Autowired
+    private RuleService ruleService;
+
+    @Autowired
+    private PluginService pluginService;
+
+    @Autowired
+    private TenantService tenantService;
+
+    @Autowired
+    private CustomerService customerService;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private DashboardService dashboardService;
+
+    @Autowired
+    private AlarmService alarmService;
+
+    @Override
+    public void deleteEntityRelations(EntityId entityId) {
+        super.deleteEntityRelations(entityId);
+    }
+
+    @Override
+    public ListenableFuture<String> fetchEntityNameAsync(EntityId entityId) {
+        log.trace("Executing fetchEntityNameAsync [{}]", entityId);
+        ListenableFuture<String> entityName;
+        ListenableFuture<? extends HasName> hasName;
+        switch (entityId.getEntityType()) {
+            case ASSET:
+                hasName = assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
+                break;
+            case DEVICE:
+                hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
+                break;
+            case RULE:
+                hasName = ruleService.findRuleByIdAsync(new RuleId(entityId.getId()));
+                break;
+            case PLUGIN:
+                hasName = pluginService.findPluginByIdAsync(new PluginId(entityId.getId()));
+                break;
+            case TENANT:
+                hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+                break;
+            case CUSTOMER:
+                hasName = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
+                break;
+            case USER:
+                hasName = userService.findUserByIdAsync(new UserId(entityId.getId()));
+                break;
+            case DASHBOARD:
+                hasName = dashboardService.findDashboardInfoByIdAsync(new DashboardId(entityId.getId()));
+                break;
+            case ALARM:
+                hasName = alarmService.findAlarmByIdAsync(new AlarmId(entityId.getId()));
+                break;
+            default:
+                throw new IllegalStateException("Not Implemented!");
+        }
+        entityName = Futures.transform(hasName, (Function<HasName, String>) hasName1 -> hasName1.getName() );
+        return entityName;
     }
 
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
new file mode 100644
index 0000000..415f518
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2017 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.dao.entity;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.EntityId;
+
+public interface EntityService {
+
+    ListenableFuture<String> fetchEntityNameAsync(EntityId entityId);
+
+    void deleteEntityRelations(EntityId entityId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
index 5e0eb39..a0be00e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
@@ -22,7 +22,6 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -30,9 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.DatabaseException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -55,7 +53,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
 
 @Service
 @Slf4j
-public class BasePluginService extends BaseEntityService implements PluginService {
+public class BasePluginService extends AbstractEntityService implements PluginService {
 
     //TODO: move to a better place.
     public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
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 998f2ef..9b4f906 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
@@ -23,9 +23,24 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
+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.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.entity.EntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
 
 import javax.annotation.Nullable;
 import java.util.*;
@@ -41,6 +56,9 @@ public class BaseRelationService implements RelationService {
     @Autowired
     private RelationDao relationDao;
 
+    @Autowired
+    private EntityService entityService;
+
     @Override
     public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType) {
         log.trace("Executing checkRelation [{}][{}][{}]", from, to, relationType);
@@ -100,6 +118,31 @@ public class BaseRelationService implements RelationService {
     }
 
     @Override
+    public ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from) {
+        log.trace("Executing findInfoByFrom [{}]", from);
+        validate(from);
+        ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByFrom(from);
+        ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
+                (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+            List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
+                    relations1.stream().forEach(relation -> futures.add(fetchRelationInfoAsync(relation)));
+            return Futures.successfulAsList(futures);
+        });
+        return relationsInfo;
+    }
+
+    private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation) {
+        ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(relation.getTo());
+        ListenableFuture<EntityRelationInfo> entityRelationInfo =
+                Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
+                            EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
+                            entityRelationInfo1.setToName(entityName1);
+                            return entityRelationInfo1;
+                        });
+        return entityRelationInfo;
+    }
+
+    @Override
     public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType) {
         log.trace("Executing findByFromAndType [{}][{}]", from, relationType);
         validate(from);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
index e3e2a1f..e89985e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.relation;
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationInfo;
 
 import java.util.List;
 
@@ -38,6 +39,8 @@ public interface RelationService {
 
     ListenableFuture<List<EntityRelation>> findByFrom(EntityId from);
 
+    ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from);
+
     ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType);
 
     ListenableFuture<List<EntityRelation>> findByTo(EntityId to);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
index fb5e9ce..350fc3a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
@@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -34,11 +33,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.DatabaseException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.AssetEntity;
 import org.thingsboard.server.dao.model.RuleMetaDataEntity;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -58,7 +56,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
 
 @Service
 @Slf4j
-public class BaseRuleService extends BaseEntityService implements RuleService {
+public class BaseRuleService extends AbstractEntityService implements RuleService {
 
     private final TenantId systemTenantId = new TenantId(NULL_UUID);
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
index bcc31a8..e6f5011 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
@@ -26,7 +26,6 @@ import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -34,9 +33,8 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.model.CustomerEntity;
 import org.thingsboard.server.dao.model.TenantEntity;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.rule.RuleService;
@@ -50,7 +48,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
 
 @Service
 @Slf4j
-public class TenantServiceImpl extends BaseEntityService implements TenantService {
+public class TenantServiceImpl extends AbstractEntityService implements TenantService {
 
     private static final String DEFAULT_TENANT_REGION = "Global";
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
index f4043a0..47e800c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.user;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -27,6 +28,8 @@ public interface UserService {
 	
 	public User findUserById(UserId userId);
 
+	public ListenableFuture<User> findUserByIdAsync(UserId userId);
+
 	public User findUserByEmail(String email);
 	
 	public User saveUser(User user);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
index 6410111..125040b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -23,6 +23,9 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
 
 import java.util.List;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -35,7 +38,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.*;
@@ -47,7 +50,7 @@ import org.springframework.stereotype.Service;
 
 @Service
 @Slf4j
-public class UserServiceImpl extends BaseEntityService implements UserService {
+public class UserServiceImpl extends AbstractEntityService implements UserService {
 
     @Autowired
     private UserDao userDao;
@@ -78,6 +81,14 @@ public class UserServiceImpl extends BaseEntityService implements UserService {
 	}
 
     @Override
+    public ListenableFuture<User> findUserByIdAsync(UserId userId) {
+        log.trace("Executing findUserByIdAsync [{}]", userId);
+        validateId(userId, "Incorrect userId " + userId);
+        ListenableFuture<UserEntity> userEntity = userDao.findByIdAsync(userId.getId());
+        return Futures.transform(userEntity, (Function<? super UserEntity, ? extends User>) input -> getData(input));
+    }
+
+    @Override
     public User saveUser(User user) {
         log.trace("Executing saveUser [{}]", user);
         userValidator.validate(user);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
index 4bf43f9..3b72574 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
@@ -95,7 +95,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
         Assert.assertEquals(0L, created.getAckTs());
         Assert.assertEquals(0L, created.getClearTs());
 
-        Alarm fetched = alarmService.findAlarmById(created.getId()).get();
+        Alarm fetched = alarmService.findAlarmByIdAsync(created.getId()).get();
         Assert.assertEquals(created, fetched);
     }
 
@@ -137,7 +137,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
         Assert.assertEquals(created, alarms.getData().get(0));
 
         alarmService.ackAlarm(created.getId(), System.currentTimeMillis()).get();
-        created = alarmService.findAlarmById(created.getId()).get();
+        created = alarmService.findAlarmByIdAsync(created.getId()).get();
 
         alarms = alarmService.findAlarms(AlarmQuery.builder()
                 .affectedEntityId(childId)
@@ -158,7 +158,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
         Assert.assertEquals(0, alarms.getData().size());
 
         alarmService.clearAlarm(created.getId(), System.currentTimeMillis()).get();
-        created = alarmService.findAlarmById(created.getId()).get();
+        created = alarmService.findAlarmByIdAsync(created.getId()).get();
 
         alarms = alarmService.findAlarms(AlarmQuery.builder()
                 .affectedEntityId(childId)

docker/.env 6(+6 -0)

diff --git a/docker/.env b/docker/.env
index ca7f2b0..1c7e512 100644
--- a/docker/.env
+++ b/docker/.env
@@ -1 +1,7 @@
 CASSANDRA_DATA_DIR=/home/docker/cassandra_volume
+
+# cassandra schema container environment variables
+CREATE_SCHEMA=true
+ADD_SYSTEM_DATA=false
+ADD_DEMO_DATA=false
+CASSANDRA_URL=cassandra
\ No newline at end of file
diff --git a/docker/cassandra/cassandra.yaml b/docker/cassandra/cassandra.yaml
new file mode 100644
index 0000000..68379d3
--- /dev/null
+++ b/docker/cassandra/cassandra.yaml
@@ -0,0 +1,132 @@
+#
+# Copyright © 2016-2017 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: Service
+metadata:
+  name: cassandra-headless
+  labels:
+    app: cassandra-headless
+spec:
+  ports:
+    - port: 9042
+      name: cql
+  clusterIP: None
+  selector:
+    app: cassandra
+---
+apiVersion: "apps/v1beta1"
+kind: StatefulSet
+metadata:
+  name: cassandra
+spec:
+  serviceName: cassandra-headless
+  replicas: 2
+  template:
+    metadata:
+      labels:
+        app: cassandra
+    spec:
+      nodeSelector:
+        machinetype: other
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "app"
+                    operator: In
+                    values:
+                    - cassandra-headless
+              topologyKey: "kubernetes.io/hostname"
+      containers:
+      - name: cassandra
+        image: cassandra:3.9
+        imagePullPolicy: Always
+        ports:
+        - containerPort: 7000
+          name: intra-node
+        - containerPort: 7001
+          name: tls-intra-node
+        - containerPort: 7199
+          name: jmx
+        - containerPort: 9042
+          name: cql
+        - containerPort: 9160
+          name: thrift
+        securityContext:
+          capabilities:
+            add:
+              - IPC_LOCK
+        lifecycle:
+          preStop:
+            exec:
+              command: ["/bin/sh", "-c", "PID=$(pidof java) && kill $PID && while ps -p $PID > /dev/null; do sleep 1; done"]
+        env:
+          - name: MAX_HEAP_SIZE
+            value: 2048M
+          - name: HEAP_NEWSIZE
+            value: 100M
+          - name: CASSANDRA_SEEDS
+            value: "cassandra-0.cassandra-headless.default.svc.cluster.local"
+          - name: CASSANDRA_CLUSTER_NAME
+            value: "Thingsboard-Cluster"
+          - name: CASSANDRA_DC
+            value: "DC1-Thingsboard-Cluster"
+          - name: CASSANDRA_RACK
+            value: "Rack-Thingsboard-Cluster"
+          - name: CASSANDRA_AUTO_BOOTSTRAP
+            value: "false"
+          - name: POD_IP
+            valueFrom:
+              fieldRef:
+                fieldPath: status.podIP
+          - name: POD_NAMESPACE
+            valueFrom:
+              fieldRef:
+                fieldPath: metadata.namespace
+        readinessProbe:
+          exec:
+            command:
+            - /bin/bash
+            - -c
+            - /ready-probe.sh
+          initialDelaySeconds: 15
+          timeoutSeconds: 5
+        volumeMounts:
+        - name: cassandra-data
+          mountPath: /var/lib/cassandra/data
+        - name: cassandra-commitlog
+          mountPath: /var/lib/cassandra/commitlog
+  volumeClaimTemplates:
+  - metadata:
+      name: cassandra-data
+      annotations:
+        volume.beta.kubernetes.io/storage-class: fast
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      resources:
+        requests:
+          storage: 3Gi
+  - metadata:
+      name: cassandra-commitlog
+      annotations:
+        volume.beta.kubernetes.io/storage-class: fast
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      resources:
+        requests:
+          storage: 2Gi
\ No newline at end of file
diff --git a/docker/docker-compose.static.yml b/docker/docker-compose.static.yml
index 80cc6a9..5471cb2 100644
--- a/docker/docker-compose.static.yml
+++ b/docker/docker-compose.static.yml
@@ -17,7 +17,7 @@
 version: '2'
 
 services:
-  db:
+  cassandra:
     ports:
       - "9042:9042"
       - "9160:9160"
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 97873e8..8367abc 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -17,24 +17,32 @@
 version: '2'
 
 services:
-  thingsboard:
-    image: "thingsboard/application:1.2.3"
+  tb:
+    image: "thingsboard/application:1.2.4"
     ports:
       - "8080:8080"
       - "1883:1883"
       - "5683:5683/udp"
     env_file:
-      - thingsboard.env
-    entrypoint: ./run_thingsboard.sh
-  thingsboard-db-schema:
-    image: "thingsboard/thingsboard-db-schema:1.2.3"
-    env_file:
-      - thingsboard-db-schema.env
-    entrypoint: ./install_schema.sh
-  db:
+      - tb.env
+    entrypoint: ./run-application.sh
+  tb-cassandra-schema:
+    image: "thingsboard/tb-cassandra-schema:1.2.4"
+    environment:
+      - CREATE_SCHEMA=${CREATE_SCHEMA}
+      - ADD_SYSTEM_DATA=${ADD_SYSTEM_DATA}
+      - ADD_DEMO_DATA=${ADD_DEMO_DATA}
+      - CASSANDRA_URL=${CASSANDRA_URL}
+    entrypoint: ./install-schema.sh
+  cassandra:
     image: "cassandra:3.9"
+    ports:
+      - "9042"
+      - "9160"
     volumes:
       - "${CASSANDRA_DATA_DIR}:/var/lib/cassandra"
   zk:
     image: "zookeeper:3.4.9"
+    ports:
+      - "2181"
     restart: always

docker/tb/Makefile 11(+11 -0)

diff --git a/docker/tb/Makefile b/docker/tb/Makefile
new file mode 100644
index 0000000..afd1f80
--- /dev/null
+++ b/docker/tb/Makefile
@@ -0,0 +1,11 @@
+VERSION=1.2.4
+PROJECT=thingsboard
+APP=application
+
+build:
+	cp ../../application/target/thingsboard.deb .
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} .
+	rm thingsboard.deb
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
\ No newline at end of file

docker/tb/tb.yaml 121(+121 -0)

diff --git a/docker/tb/tb.yaml b/docker/tb/tb.yaml
new file mode 100644
index 0000000..15ed193
--- /dev/null
+++ b/docker/tb/tb.yaml
@@ -0,0 +1,121 @@
+#
+# Copyright © 2016-2017 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: Service
+metadata:
+  name: tb-service
+  labels:
+    app: tb-service
+spec:
+  ports:
+  - port: 8080
+    name: ui
+  - port: 1883
+    name: mqtt
+  - port: 5683
+    name: coap
+  selector:
+    app: tb
+  type: LoadBalancer
+---
+apiVersion: policy/v1beta1
+kind: PodDisruptionBudget
+metadata:
+  name: tb-budget
+spec:
+  selector:
+    matchLabels:
+      app: tb
+  minAvailable: 3
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: tb-config
+data:
+  zookeeper.enabled: "true"
+  zookeeper.url: "zk-headless"
+  cassandra.url: "cassandra-headless:9042"
+---
+apiVersion: apps/v1beta1
+kind: StatefulSet
+metadata:
+  name: tb
+spec:
+  serviceName: "tb-service"
+  replicas: 3
+  template:
+    metadata:
+      labels:
+        app: tb
+    spec:
+      nodeSelector:
+        machinetype: tb
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "app"
+                    operator: In
+                    values:
+                    - tb-service
+              topologyKey: "kubernetes.io/hostname"
+      containers:
+      - name: tb
+        imagePullPolicy: Always
+        image: thingsboard/application:1.2.4
+        ports:
+        - containerPort: 8080
+          name: ui
+        - containerPort: 1883
+          name: mqtt
+        - containerPort: 5683
+          name: coap
+        - containerPort: 9001
+          name: rpc
+        env:
+        - name: ZOOKEEPER_ENABLED
+          valueFrom:
+            configMapKeyRef:
+              name: tb-config
+              key: zookeeper.enabled
+        - name: ZOOKEEPER_URL
+          valueFrom:
+            configMapKeyRef:
+              name: tb-config
+              key: zookeeper.url
+        - name : CASSANDRA_URL
+          valueFrom:
+            configMapKeyRef:
+              name: tb-config
+              key: cassandra.url
+        - name : RPC_HOST
+          valueFrom:
+            fieldRef:
+              fieldPath: status.podIP
+        command:
+        - sh
+        - -c
+        - ./run-application.sh
+        livenessProbe:
+          httpGet:
+            path: /login
+            port: ui-port
+          initialDelaySeconds: 120
+          timeoutSeconds: 10
\ No newline at end of file
diff --git a/docker/tb-cassandra-schema/Makefile b/docker/tb-cassandra-schema/Makefile
new file mode 100644
index 0000000..c3f2820
--- /dev/null
+++ b/docker/tb-cassandra-schema/Makefile
@@ -0,0 +1,13 @@
+VERSION=1.2.4
+PROJECT=thingsboard
+APP=tb-cassandra-schema
+
+build:
+	cp ../../dao/src/main/resources/schema.cql .
+	cp ../../dao/src/main/resources/demo-data.cql .
+	cp ../../dao/src/main/resources/system-data.cql .
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} .
+	rm schema.cql demo-data.cql system-data.cql
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
\ No newline at end of file
diff --git a/docker/tb-cassandra-schema/tb-cassandra-schema.yaml b/docker/tb-cassandra-schema/tb-cassandra-schema.yaml
new file mode 100644
index 0000000..e1e2722
--- /dev/null
+++ b/docker/tb-cassandra-schema/tb-cassandra-schema.yaml
@@ -0,0 +1,39 @@
+#
+# Copyright © 2016-2017 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: tb-cassandra-schema
+spec:
+  containers:
+  - name: tb-cassandra-schema
+    imagePullPolicy: Always
+    image: thingsboard/tb-cassandra-schema:1.2.4
+    env:
+    - name: CREATE_SCHEMA
+      value: "false"
+    - name: ADD_SYSTEM_DATA
+      value: "false"
+    - name : ADD_DEMO_DATA
+      value: "false"
+    - name : CASSANDRA_URL
+      value: "cassandra-headless"
+    command:
+    - sh
+    - -c
+    - ./install-schema.sh
+  restartPolicy: Never
\ No newline at end of file
diff --git a/docker/zookeeper/Dockerfile b/docker/zookeeper/Dockerfile
new file mode 100644
index 0000000..5ec1ac9
--- /dev/null
+++ b/docker/zookeeper/Dockerfile
@@ -0,0 +1,71 @@
+#
+# Copyright © 2016-2017 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 ubuntu:16.04 
+ENV ZK_USER=zookeeper \
+ZK_DATA_DIR=/var/lib/zookeeper/data \
+ZK_DATA_LOG_DIR=/var/lib/zookeeper/log \
+ZK_LOG_DIR=/var/log/zookeeper \
+JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
+
+ARG GPG_KEY=C823E3E5B12AF29C67F81976F5CECB3CB5E9BD2D
+ARG ZK_DIST=zookeeper-3.4.9
+RUN set -x \
+    && apt-get update \
+    && apt-get install -y openjdk-8-jre-headless wget netcat-openbsd \
+	&& wget -q "http://www.apache.org/dist/zookeeper/$ZK_DIST/$ZK_DIST.tar.gz" \
+    && wget -q "http://www.apache.org/dist/zookeeper/$ZK_DIST/$ZK_DIST.tar.gz.asc" \
+    && export GNUPGHOME="$(mktemp -d)" \
+    && gpg --keyserver ha.pool.sks-keyservers.net --recv-key "$GPG_KEY" \
+    && gpg --batch --verify "$ZK_DIST.tar.gz.asc" "$ZK_DIST.tar.gz" \
+    && tar -xzf "$ZK_DIST.tar.gz" -C /opt \
+    && rm -r "$GNUPGHOME" "$ZK_DIST.tar.gz" "$ZK_DIST.tar.gz.asc" \
+    && ln -s /opt/$ZK_DIST /opt/zookeeper \
+    && rm -rf /opt/zookeeper/CHANGES.txt \
+    /opt/zookeeper/README.txt \
+    /opt/zookeeper/NOTICE.txt \
+    /opt/zookeeper/CHANGES.txt \
+    /opt/zookeeper/README_packaging.txt \
+    /opt/zookeeper/build.xml \
+    /opt/zookeeper/config \
+    /opt/zookeeper/contrib \
+    /opt/zookeeper/dist-maven \
+    /opt/zookeeper/docs \
+    /opt/zookeeper/ivy.xml \
+    /opt/zookeeper/ivysettings.xml \
+    /opt/zookeeper/recipes \
+    /opt/zookeeper/src \
+    /opt/zookeeper/$ZK_DIST.jar.asc \
+    /opt/zookeeper/$ZK_DIST.jar.md5 \
+    /opt/zookeeper/$ZK_DIST.jar.sha1 \
+	&& apt-get autoremove -y wget \
+	&& rm -rf /var/lib/apt/lists/*
+
+#Copy configuration generator script to bin
+COPY zk-gen-config.sh zk-ok.sh /opt/zookeeper/bin/
+
+# Create a user for the zookeeper process and configure file system ownership 
+# for nessecary directories and symlink the distribution as a user executable
+RUN set -x \
+	&& useradd $ZK_USER \
+    && [ `id -u $ZK_USER` -eq 1000 ] \
+    && [ `id -g $ZK_USER` -eq 1000 ] \
+    && mkdir -p $ZK_DATA_DIR $ZK_DATA_LOG_DIR $ZK_LOG_DIR /usr/share/zookeeper /tmp/zookeeper /usr/etc/ \
+	&& chown -R "$ZK_USER:$ZK_USER" /opt/$ZK_DIST $ZK_DATA_DIR $ZK_LOG_DIR $ZK_DATA_LOG_DIR /tmp/zookeeper \
+	&& ln -s /opt/zookeeper/conf/ /usr/etc/zookeeper \
+	&& ln -s /opt/zookeeper/bin/* /usr/bin \
+	&& ln -s /opt/zookeeper/$ZK_DIST.jar /usr/share/zookeeper/ \
+	&& ln -s /opt/zookeeper/lib/* /usr/share/zookeeper 
diff --git a/docker/zookeeper/Makefile b/docker/zookeeper/Makefile
new file mode 100644
index 0000000..6e4ef12
--- /dev/null
+++ b/docker/zookeeper/Makefile
@@ -0,0 +1,9 @@
+VERSION=1.2.4
+PROJECT=thingsboard
+APP=zk
+
+build:
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} .
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
diff --git a/docker/zookeeper/zk-gen-config.sh b/docker/zookeeper/zk-gen-config.sh
new file mode 100755
index 0000000..02fde70
--- /dev/null
+++ b/docker/zookeeper/zk-gen-config.sh
@@ -0,0 +1,153 @@
+#!/usr/bin/env bash
+#
+# Copyright © 2016-2017 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.
+#
+
+ZK_USER=${ZK_USER:-"zookeeper"}
+ZK_LOG_LEVEL=${ZK_LOG_LEVEL:-"INFO"}
+ZK_DATA_DIR=${ZK_DATA_DIR:-"/var/lib/zookeeper/data"}
+ZK_DATA_LOG_DIR=${ZK_DATA_LOG_DIR:-"/var/lib/zookeeper/log"}
+ZK_LOG_DIR=${ZK_LOG_DIR:-"var/log/zookeeper"}
+ZK_CONF_DIR=${ZK_CONF_DIR:-"/opt/zookeeper/conf"}
+ZK_CLIENT_PORT=${ZK_CLIENT_PORT:-2181}
+ZK_SERVER_PORT=${ZK_SERVER_PORT:-2888}
+ZK_ELECTION_PORT=${ZK_ELECTION_PORT:-3888}
+ZK_TICK_TIME=${ZK_TICK_TIME:-2000}
+ZK_INIT_LIMIT=${ZK_INIT_LIMIT:-10}
+ZK_SYNC_LIMIT=${ZK_SYNC_LIMIT:-5}
+ZK_HEAP_SIZE=${ZK_HEAP_SIZE:-2G}
+ZK_MAX_CLIENT_CNXNS=${ZK_MAX_CLIENT_CNXNS:-60}
+ZK_MIN_SESSION_TIMEOUT=${ZK_MIN_SESSION_TIMEOUT:- $((ZK_TICK_TIME*2))}
+ZK_MAX_SESSION_TIMEOUT=${ZK_MAX_SESSION_TIMEOUT:- $((ZK_TICK_TIME*20))}
+ZK_SNAP_RETAIN_COUNT=${ZK_SNAP_RETAIN_COUNT:-3}
+ZK_PURGE_INTERVAL=${ZK_PURGE_INTERVAL:-0}
+ID_FILE="$ZK_DATA_DIR/myid"
+ZK_CONFIG_FILE="$ZK_CONF_DIR/zoo.cfg"
+LOGGER_PROPS_FILE="$ZK_CONF_DIR/log4j.properties"
+JAVA_ENV_FILE="$ZK_CONF_DIR/java.env"
+HOST=`hostname -s`
+DOMAIN=`hostname -d`
+
+function print_servers() {
+	 for (( i=1; i<=$ZK_REPLICAS; i++ ))
+	do
+		echo "server.$i=$NAME-$((i-1)).$DOMAIN:$ZK_SERVER_PORT:$ZK_ELECTION_PORT"
+	done
+}
+
+function validate_env() {
+    echo "Validating environment"
+	if [ -z $ZK_REPLICAS ]; then
+		echo "ZK_REPLICAS is a mandatory environment variable"
+		exit 1
+	fi
+
+	if [[ $HOST =~ (.*)-([0-9]+)$ ]]; then
+		NAME=${BASH_REMATCH[1]}
+		ORD=${BASH_REMATCH[2]}
+	else
+		echo "Failed to extract ordinal from hostname $HOST"
+		exit 1
+	fi
+	MY_ID=$((ORD+1))
+	echo "ZK_REPLICAS=$ZK_REPLICAS"
+    echo "MY_ID=$MY_ID"
+    echo "ZK_LOG_LEVEL=$ZK_LOG_LEVEL"
+    echo "ZK_DATA_DIR=$ZK_DATA_DIR"
+    echo "ZK_DATA_LOG_DIR=$ZK_DATA_LOG_DIR"
+    echo "ZK_LOG_DIR=$ZK_LOG_DIR"
+    echo "ZK_CLIENT_PORT=$ZK_CLIENT_PORT"
+    echo "ZK_SERVER_PORT=$ZK_SERVER_PORT"
+    echo "ZK_ELECTION_PORT=$ZK_ELECTION_PORT"
+    echo "ZK_TICK_TIME=$ZK_TICK_TIME"
+    echo "ZK_INIT_LIMIT=$ZK_INIT_LIMIT"
+    echo "ZK_SYNC_LIMIT=$ZK_SYNC_LIMIT"
+    echo "ZK_MAX_CLIENT_CNXNS=$ZK_MAX_CLIENT_CNXNS"
+    echo "ZK_MIN_SESSION_TIMEOUT=$ZK_MIN_SESSION_TIMEOUT"
+    echo "ZK_MAX_SESSION_TIMEOUT=$ZK_MAX_SESSION_TIMEOUT"
+    echo "ZK_HEAP_SIZE=$ZK_HEAP_SIZE"
+    echo "ZK_SNAP_RETAIN_COUNT=$ZK_SNAP_RETAIN_COUNT"
+    echo "ZK_PURGE_INTERVAL=$ZK_PURGE_INTERVAL"
+    echo "ENSEMBLE"
+    print_servers
+    echo "Environment validation successful"
+}
+
+function create_config() {
+	rm -f $ZK_CONFIG_FILE
+    echo "Creating ZooKeeper configuration"
+    echo "#This file was autogenerated by zk DO NOT EDIT" >> $ZK_CONFIG_FILE
+	echo "clientPort=$ZK_CLIENT_PORT" >> $ZK_CONFIG_FILE
+    echo "dataDir=$ZK_DATA_DIR" >> $ZK_CONFIG_FILE
+    echo "dataLogDir=$ZK_DATA_LOG_DIR" >> $ZK_CONFIG_FILE
+    echo "tickTime=$ZK_TICK_TIME" >> $ZK_CONFIG_FILE
+    echo "initLimit=$ZK_INIT_LIMIT" >> $ZK_CONFIG_FILE
+    echo "syncLimit=$ZK_SYNC_LIMIT" >> $ZK_CONFIG_FILE
+    echo "maxClientCnxns=$ZK_MAX_CLIENT_CNXNS" >> $ZK_CONFIG_FILE
+    echo "minSessionTimeout=$ZK_MIN_SESSION_TIMEOUT" >> $ZK_CONFIG_FILE
+    echo "maxSessionTimeout=$ZK_MAX_SESSION_TIMEOUT" >> $ZK_CONFIG_FILE
+    echo "autopurge.snapRetainCount=$ZK_SNAP_RETAIN_COUNT" >> $ZK_CONFIG_FILE
+    echo "autopurge.purgeInteval=$ZK_PURGE_INTERVAL" >> $ZK_CONFIG_FILE
+
+    if [ $ZK_REPLICAS -gt 1 ]; then
+    	print_servers >> $ZK_CONFIG_FILE
+    fi
+    echo "Wrote ZooKeeper configuration file to $ZK_CONFIG_FILE"
+}
+
+function create_data_dirs() {
+	echo "Creating ZooKeeper data directories and setting permissions"
+    if [ ! -d $ZK_DATA_DIR  ]; then
+        mkdir -p $ZK_DATA_DIR
+        chown -R $ZK_USER:$ZK_USER $ZK_DATA_DIR
+    fi
+
+    if [ ! -d $ZK_DATA_LOG_DIR  ]; then
+        mkdir -p $ZK_DATA_LOG_DIR
+        chown -R $ZK_USER:$ZK_USER $ZK_DATA_LOG_DIR
+    fi
+
+    if [ ! -d $ZK_LOG_DIR  ]; then
+        mkdir -p $ZK_LOG_DIR
+        chown -R $ZK_USER:$ZK_USER $ZK_LOG_DIR
+    fi
+    if [ ! -f $ID_FILE ]; then
+        echo $MY_ID >> $ID_FILE
+    fi
+    echo "Created ZooKeeper data directories and set permissions in $ZK_DATA_DIR"
+}
+
+function create_log_props () {
+	rm -f $LOGGER_PROPS_FILE
+    echo "Creating ZooKeeper log4j configuration"
+	echo "zookeeper.root.logger=CONSOLE" >> $LOGGER_PROPS_FILE
+	echo "zookeeper.console.threshold="$ZK_LOG_LEVEL >> $LOGGER_PROPS_FILE
+	echo "log4j.rootLogger=\${zookeeper.root.logger}" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE.Threshold=\${zookeeper.console.threshold}" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n" >> $LOGGER_PROPS_FILE
+	echo "Wrote log4j configuration to $LOGGER_PROPS_FILE"
+}
+
+function create_java_env() {
+    rm -f $JAVA_ENV_FILE
+    echo "Creating JVM configuration file"
+    echo "ZOO_LOG_DIR=$ZK_LOG_DIR" >> $JAVA_ENV_FILE
+    echo "JVMFLAGS=\"-Xmx$ZK_HEAP_SIZE -Xms$ZK_HEAP_SIZE\"" >> $JAVA_ENV_FILE
+    echo "Wrote JVM configuration to $JAVA_ENV_FILE"
+}
+
+validate_env && create_config && create_log_props && create_data_dirs && create_java_env
diff --git a/docker/zookeeper/zookeeper.yaml b/docker/zookeeper/zookeeper.yaml
new file mode 100644
index 0000000..d96a744
--- /dev/null
+++ b/docker/zookeeper/zookeeper.yaml
@@ -0,0 +1,190 @@
+#
+# Copyright © 2016-2017 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: Service
+metadata:
+  name: zk-headless
+  labels:
+    app: zk-headless
+spec:
+  ports:
+  - port: 2888
+    name: server
+  - port: 3888
+    name: leader-election
+  clusterIP: None
+  selector:
+    app: zk
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: zk-config
+data:
+  ensemble: "zk-0;zk-1;zk-2"
+  replicas: "3"
+  jvm.heap: "500m"
+  tick: "2000"
+  init: "10"
+  sync: "5"
+  client.cnxns: "60"
+  snap.retain: "3"
+  purge.interval: "1"
+  client.port: "2181"
+  server.port: "2888"
+  election.port: "3888"
+---
+apiVersion: policy/v1beta1
+kind: PodDisruptionBudget
+metadata:
+  name: zk-budget
+spec:
+  selector:
+    matchLabels:
+      app: zk
+  minAvailable: 3
+---
+apiVersion: apps/v1beta1
+kind: StatefulSet
+metadata:
+  name: zk
+spec:
+  serviceName: zk-headless
+  replicas: 3
+  template:
+    metadata:
+      labels:
+        app: zk
+      annotations:
+        pod.alpha.kubernetes.io/initialized: "true"
+    spec:
+      nodeSelector:
+        machinetype: other
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "app"
+                    operator: In
+                    values:
+                    - zk-headless
+              topologyKey: "kubernetes.io/hostname"
+      containers:
+      - name: zk
+        imagePullPolicy: Always
+        image: thingsboard/zk:1.2.4
+        ports:
+        - containerPort: 2181
+          name: client
+        - containerPort: 2888
+          name: server
+        - containerPort: 3888
+          name: leader-election
+        env:
+        - name : ZK_ENSEMBLE
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: ensemble
+        - name : ZK_REPLICAS
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: replicas
+        - name : ZK_HEAP_SIZE
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: jvm.heap
+        - name : ZK_TICK_TIME
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: tick
+        - name : ZK_INIT_LIMIT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: init
+        - name : ZK_SYNC_LIMIT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: tick
+        - name : ZK_MAX_CLIENT_CNXNS
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: client.cnxns
+        - name: ZK_SNAP_RETAIN_COUNT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: snap.retain
+        - name: ZK_PURGE_INTERVAL
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: purge.interval
+        - name: ZK_CLIENT_PORT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: client.port
+        - name: ZK_SERVER_PORT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: server.port
+        - name: ZK_ELECTION_PORT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: election.port
+        command:
+        - sh
+        - -c
+        - zk-gen-config.sh && zkServer.sh start-foreground
+        readinessProbe:
+          exec:
+            command:
+            - "zk-ok.sh"
+          initialDelaySeconds: 15
+          timeoutSeconds: 5
+        livenessProbe:
+          exec:
+            command:
+            - "zk-ok.sh"
+          initialDelaySeconds: 15
+          timeoutSeconds: 5
+        volumeMounts:
+        - name: zkdatadir
+          mountPath: /var/lib/zookeeper
+      securityContext:
+        runAsUser: 1000
+        fsGroup: 1000
+  volumeClaimTemplates:
+  - metadata:
+      name: zkdatadir
+      annotations:
+        volume.beta.kubernetes.io/storage-class: slow
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      resources:
+        requests:
+          storage: 1Gi
\ No newline at end of file
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
index 80e4e01..aed9a0c 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
@@ -41,7 +41,7 @@ import java.security.cert.X509Certificate;
  */
 @Slf4j
 @Component("MqttSslHandlerProvider")
-@ConditionalOnProperty(prefix = "mqtt.ssl", value = "key-store", havingValue = "", matchIfMissing = false)
+@ConditionalOnProperty(prefix = "mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class MqttSslHandlerProvider {
 
     public static final String TLS = "TLS";
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index a70308b..891f9d8 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -20,14 +20,13 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
     .name;
 
 /*@ngInject*/
-function EntityService($http, $q, $filter, $translate, userService, deviceService,
+function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
                        assetService, tenantService, customerService,
-                       ruleService, pluginService, entityRelationService, attributeService, types, utils) {
+                       ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
     var service = {
         getEntity: getEntity,
         getEntities: getEntities,
         getEntitiesByNameFilter: getEntitiesByNameFilter,
-        entityName: entityName,
         processEntityAliases: processEntityAliases,
         getEntityKeys: getEntityKeys,
         checkEntityAlias: checkEntityAlias,
@@ -63,6 +62,15 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
             case types.entityType.plugin:
                 promise = pluginService.getPlugin(entityId);
                 break;
+            case types.entityType.dashboard:
+                promise = dashboardService.getDashboardInfo(entityId);
+                break;
+            case types.entityType.user:
+                promise = userService.getUser(entityId);
+                break;
+            case types.entityType.alarm:
+                $log.error('Get Alarm Entity is not implemented!');
+                break;
         }
         return promise;
     }
@@ -134,6 +142,15 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
             case types.entityType.plugin:
                 promise = getEntitiesByIdsPromise(pluginService.getPlugin, entityIds);
                 break;
+            case types.entityType.dashboard:
+                promise = getEntitiesByIdsPromise(dashboardService.getDashboardInfo, entityIds);
+                break;
+            case types.entityType.user:
+                promise = getEntitiesByIdsPromise(userService.getUser, entityIds);
+                break;
+            case types.entityType.alarm:
+                $log.error('Get Alarm Entity is not implemented!');
+                break;
         }
         return promise;
     }
@@ -141,34 +158,38 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
     function getEntities(entityType, entityIds, config) {
         var deferred = $q.defer();
         var promise = getEntitiesPromise(entityType, entityIds, config);
-        promise.then(
-            function success(result) {
-                deferred.resolve(result);
-            },
-            function fail() {
-                deferred.reject();
-            }
-        );
+        if (promise) {
+            promise.then(
+                function success(result) {
+                    deferred.resolve(result);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        } else {
+            deferred.reject();
+        }
         return deferred.promise;
     }
 
-    function getEntitiesByPageLinkPromise(entityType, pageLink, config) {
+    function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) {
         var promise;
         var user = userService.getCurrentUser();
         var customerId = user.customerId;
         switch (entityType) {
             case types.entityType.device:
                 if (user.authority === 'CUSTOMER_USER') {
-                    promise = deviceService.getCustomerDevices(customerId, pageLink, false, config);
+                    promise = deviceService.getCustomerDevices(customerId, pageLink, false, config, subType);
                 } else {
-                    promise = deviceService.getTenantDevices(pageLink, false, config);
+                    promise = deviceService.getTenantDevices(pageLink, false, config, subType);
                 }
                 break;
             case types.entityType.asset:
                 if (user.authority === 'CUSTOMER_USER') {
-                    promise = assetService.getCustomerAssets(customerId, pageLink, false, config);
+                    promise = assetService.getCustomerAssets(customerId, pageLink, false, config, subType);
                 } else {
-                    promise = assetService.getTenantAssets(pageLink, false, config);
+                    promise = assetService.getTenantAssets(pageLink, false, config, subType);
                 }
                 break;
             case types.entityType.tenant:
@@ -183,48 +204,48 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
             case types.entityType.plugin:
                 promise = pluginService.getAllPlugins(pageLink);
                 break;
+            case types.entityType.dashboard:
+                if (user.authority === 'CUSTOMER_USER') {
+                    promise = dashboardService.getCustomerDashboards(customerId, pageLink);
+                } else {
+                    promise = dashboardService.getTenantDashboards(pageLink);
+                }
+                break;
+            case types.entityType.user:
+                $log.error('Get User Entities is not implemented!');
+                break;
+            case types.entityType.alarm:
+                $log.error('Get Alarm Entities is not implemented!');
+                break;
         }
         return promise;
     }
 
-    function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config) {
+    function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config, subType) {
         var deferred = $q.defer();
         var pageLink = {limit: limit, textSearch: entityNameFilter};
-        var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config);
-        promise.then(
-            function success(result) {
-                if (result.data && result.data.length > 0) {
-                    deferred.resolve(result.data);
-                } else {
+        var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config, subType);
+        if (promise) {
+            promise.then(
+                function success(result) {
+                    if (result.data && result.data.length > 0) {
+                        deferred.resolve(result.data);
+                    } else {
+                        deferred.resolve(null);
+                    }
+                },
+                function fail() {
                     deferred.resolve(null);
                 }
-            },
-            function fail() {
-                deferred.resolve(null);
-            }
-        );
-        return deferred.promise;
-    }
-
-    function entityName(entityType, entity) {
-        var name = '';
-        switch (entityType) {
-            case types.entityType.device:
-            case types.entityType.asset:
-            case types.entityType.rule:
-            case types.entityType.plugin:
-                name = entity.name;
-                break;
-            case types.entityType.tenant:
-            case types.entityType.customer:
-                name = entity.title;
-                break;
+            );
+        } else {
+            deferred.resolve(null);
         }
-        return name;
+        return deferred.promise;
     }
 
     function entityToEntityInfo(entityType, entity) {
-        return { name: entityName(entityType, entity), entityType: entityType, id: entity.id.id };
+        return { name: entity.name, entityType: entityType, id: entity.id.id };
     }
 
     function entitiesToEntitiesInfo(entityType, entities) {
diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js
index 998607a..7039645 100644
--- a/ui/src/app/api/entity-relation.service.js
+++ b/ui/src/app/api/entity-relation.service.js
@@ -25,6 +25,7 @@ function EntityRelationService($http, $q) {
         deleteRelation: deleteRelation,
         deleteRelations: deleteRelations,
         findByFrom: findByFrom,
+        findInfoByFrom: findInfoByFrom,
         findByFromAndType: findByFromAndType,
         findByTo: findByTo,
         findByToAndType: findByToAndType,
@@ -84,6 +85,18 @@ function EntityRelationService($http, $q) {
         return deferred.promise;
     }
 
+    function findInfoByFrom(fromId, fromType) {
+        var deferred = $q.defer();
+        var url = '/api/relations/info?fromId=' + fromId;
+        url += '&fromType=' + fromType;
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
     function findByFromAndType(fromId, fromType, relationType) {
         var deferred = $q.defer();
         var url = '/api/relations?fromId=' + fromId;
diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html
index fb3cf56..11a118f 100644
--- a/ui/src/app/asset/assets.tpl.html
+++ b/ui/src/app/asset/assets.tpl.html
@@ -55,4 +55,10 @@
                             default-event-type="{{vm.types.eventType.alarm.value}}">
             </tb-event-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+            <tb-relation-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.asset}}">
+            </tb-relation-table>
+        </md-tab>
 </tb-grid>
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index a8d8556..47ac7a0 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -98,7 +98,10 @@ export default angular.module('thingsboard.types', [])
                 rule: "RULE",
                 plugin: "PLUGIN",
                 tenant: "TENANT",
-                customer: "CUSTOMER"
+                customer: "CUSTOMER",
+                user: "USER",
+                dashboard: "DASHBOARD",
+                alarm: "ALARM"
             },
             entitySearchDirection: {
                 from: "FROM",
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index b324cbf..7b4d65e 100644
--- a/ui/src/app/common/utils.service.js
+++ b/ui/src/app/common/utils.service.js
@@ -108,7 +108,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
         guid: guid,
         isLocalUrl: isLocalUrl,
         validateDatasources: validateDatasources,
-        createKey: createKey
+        createKey: createKey,
+        entityTypeName: entityTypeName
     }
 
     return service;
@@ -346,4 +347,27 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
         return dataKey;
     }
 
+    function entityTypeName (type) {
+        switch (type) {
+            case types.entityType.device:
+                return 'entity.type-device';
+            case types.entityType.asset:
+                return 'entity.type-asset';
+            case types.entityType.rule:
+                return 'entity.type-rule';
+            case types.entityType.plugin:
+                return 'entity.type-plugin';
+            case types.entityType.tenant:
+                return 'entity.type-tenant';
+            case types.entityType.customer:
+                return 'entity.type-customer';
+            case types.entityType.user:
+                return 'entity.type-user';
+            case types.entityType.dashboard:
+                return 'entity.type-dashboard';
+            case types.entityType.alarm:
+                return 'entity.type-alarm';
+        }
+    }
+
 }
diff --git a/ui/src/app/components/dashboard-autocomplete.tpl.html b/ui/src/app/components/dashboard-autocomplete.tpl.html
index 8b6be20..d1193c2 100644
--- a/ui/src/app/components/dashboard-autocomplete.tpl.html
+++ b/ui/src/app/components/dashboard-autocomplete.tpl.html
@@ -34,7 +34,7 @@
     </md-item-template>
     <md-not-found>
         <div class="tb-not-found">
-            <span translate translate-values='{ dashboard: dashboardSearchText }'>dashboard.no-dashboards-matching</span>
+            <span translate translate-values='{ entity: dashboardSearchText }'>dashboard.no-dashboards-matching</span>
         </div>
     </md-not-found>
     <div ng-messages="theForm.dashboard.$error">
diff --git a/ui/src/app/components/plugin-select.tpl.html b/ui/src/app/components/plugin-select.tpl.html
index 420442e..9a46d7f 100644
--- a/ui/src/app/components/plugin-select.tpl.html
+++ b/ui/src/app/components/plugin-select.tpl.html
@@ -34,7 +34,7 @@
     </md-item-template>
     <md-not-found>
         <div class="tb-not-found">
-            <span translate translate-values='{ plugin: pluginSearchText }'>plugin.no-plugins-matching</span>
+            <span translate translate-values='{ entity: pluginSearchText }'>plugin.no-plugins-matching</span>
         </div>
     </md-not-found>
 </md-autocomplete>
diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js
index 3eaf453..8ee9285 100644
--- a/ui/src/app/dashboard/states/entity-state-controller.js
+++ b/ui/src/app/dashboard/states/entity-state-controller.js
@@ -109,7 +109,7 @@ export default function EntityStateController($scope, $location, $state, $stateP
         if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
             entityService.getEntity(params.entityId.entityType, params.entityId.id, {ignoreLoading: true, ignoreErrors: true}).then(
                 function success(entity) {
-                    var entityName = entityService.entityName(params.entityId.entityType, entity);
+                    var entityName = entity.name;
                     deferred.resolve(entityName);
                 },
                 function fail() {
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
index 3b10902..1afd1bb 100644
--- a/ui/src/app/entity/attribute/attribute-table.tpl.html
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -128,9 +128,9 @@
             <table md-table md-row-select multiple="" ng-model="selectedAttributes" md-progress="attributesDeferred.promise">
                 <thead md-head md-order="query.order" md-on-reorder="onReorder">
                     <tr md-row>
-                        <th md-column md-order-by="lastUpdateTs"><span>Last update time</span></th>
-                        <th md-column md-order-by="key"><span>Key</span></th>
-                        <th md-column>Value</th>
+                        <th md-column md-order-by="lastUpdateTs"><span translate>attribute.last-update-time</span></th>
+                        <th md-column md-order-by="key"><span translate>attribute.key</span></th>
+                        <th md-column><span translate>attribute.value</span></th>
                     </tr>
                 </thead>
                 <tbody md-body>
diff --git a/ui/src/app/entity/entity-aliases.controller.js b/ui/src/app/entity/entity-aliases.controller.js
index 4eb1737..f1e2e38 100644
--- a/ui/src/app/entity/entity-aliases.controller.js
+++ b/ui/src/app/entity/entity-aliases.controller.js
@@ -110,7 +110,7 @@ export default function EntityAliasesController(utils, entityService, toast, $sc
                 entityAlias.changed = false;
             }
             if (!entityAlias.changed && entity && entityAlias.entityType) {
-                entityAlias.alias = entityService.entityName(entityAlias.entityType, entity);
+                entityAlias.alias = entity.name;
             }
         }
     }
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
new file mode 100644
index 0000000..350bde9
--- /dev/null
+++ b/ui/src/app/entity/entity-autocomplete.directive.js
@@ -0,0 +1,171 @@
+/*
+ * Copyright © 2016-2017 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 './entity-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entityAutocompleteTemplate from './entity-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntityAutocomplete($compile, $templateCache, $q, $filter, entityService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entityAutocompleteTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.entity = null;
+        scope.entitySearchText = '';
+
+        scope.fetchEntities = function(searchText) {
+            var deferred = $q.defer();
+            entityService.getEntitiesByNameFilter(scope.entityType, searchText, 50, null, scope.entitySubtype).then(function success(result) {
+                if (result) {
+                    deferred.resolve(result);
+                } else {
+                    deferred.resolve([]);
+                }
+            }, function fail() {
+                deferred.reject();
+            });
+            return deferred.promise;
+        }
+
+        scope.entitySearchTextChanged = function() {
+        }
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id.id : null);
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                entityService.getEntity(scope.entityType, ngModelCtrl.$viewValue).then(
+                    function success(entity) {
+                        scope.entity = entity;
+                    },
+                    function fail() {
+                        scope.entity = null;
+                    }
+                );
+            } else {
+                scope.entity = null;
+            }
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('entitySubtype', function () {
+            if (scope.entity && scope.entity.type != scope.entitySubtype) {
+                scope.entity = null;
+                scope.updateView();
+            }
+        });
+
+        scope.$watch('entity', function () {
+            scope.updateView();
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+
+        function load() {
+            switch (scope.entityType) {
+                case types.entityType.asset:
+                    scope.selectEntityText = 'asset.select-asset';
+                    scope.entityText = 'asset.asset';
+                    scope.noEntitiesMatchingText = 'asset.no-assets-matching';
+                    scope.entityRequiredText = 'asset.asset-required'
+                    break;
+                case types.entityType.device:
+                    scope.selectEntityText = 'device.select-device';
+                    scope.entityText = 'device.device';
+                    scope.noEntitiesMatchingText = 'device.no-devices-matching';
+                    scope.entityRequiredText = 'device.device-required'
+                    break;
+                case types.entityType.rule:
+                    scope.selectEntityText = 'rule.select-rule';
+                    scope.entityText = 'rule.rule';
+                    scope.noEntitiesMatchingText = 'rule.no-rules-matching';
+                    scope.entityRequiredText = 'rule.rule-required'
+                    break;
+                case types.entityType.plugin:
+                    scope.selectEntityText = 'plugin.select-plugin';
+                    scope.entityText = 'plugin.plugin';
+                    scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
+                    scope.entityRequiredText = 'plugin.plugin-required'
+                    break;
+                case types.entityType.tenant:
+                    scope.selectEntityText = 'tenant.select-tenant';
+                    scope.entityText = 'tenant.tenant';
+                    scope.noEntitiesMatchingText = 'tenant.no-tenants-matching';
+                    scope.entityRequiredText = 'tenant.tenant-required'
+                    break;
+                case types.entityType.customer:
+                    scope.selectEntityText = 'customer.select-customer';
+                    scope.entityText = 'customer.customer';
+                    scope.noEntitiesMatchingText = 'customer.no-customers-matching';
+                    scope.entityRequiredText = 'customer.customer-required'
+                    break;
+                case types.entityType.user:
+                    scope.selectEntityText = 'user.select-user';
+                    scope.entityText = 'user.user';
+                    scope.noEntitiesMatchingText = 'user.no-users-matching';
+                    scope.entityRequiredText = 'user.user-required'
+                    break;
+                case types.entityType.dashboard:
+                    scope.selectEntityText = 'dashboard.select-dashboard';
+                    scope.entityText = 'dashboard.dashboard';
+                    scope.noEntitiesMatchingText = 'dashboard.no-dashboards-matching';
+                    scope.entityRequiredText = 'dashboard.dashboard-required'
+                    break;
+                case types.entityType.alarm:
+                    scope.selectEntityText = 'alarm.select-alarm';
+                    scope.entityText = 'alarm.alarm';
+                    scope.noEntitiesMatchingText = 'alarm.no-alarms-matching';
+                    scope.entityRequiredText = 'alarm.alarm-required'
+                    break;
+            }
+            if (scope.entity && scope.entity.id.entityType != scope.entityType) {
+                scope.entity = null;
+                scope.updateView();
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled',
+            entityType: '=',
+            entitySubtype: '=?'
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-autocomplete.scss b/ui/src/app/entity/entity-autocomplete.scss
new file mode 100644
index 0000000..c6affa5
--- /dev/null
+++ b/ui/src/app/entity/entity-autocomplete.scss
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016-2017 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-entity-autocomplete {
+  .tb-entity-item {
+    display: block;
+    height: 48px;
+  }
+  li {
+    height: auto !important;
+    white-space: normal !important;
+  }
+}
diff --git a/ui/src/app/entity/entity-autocomplete.tpl.html b/ui/src/app/entity/entity-autocomplete.tpl.html
new file mode 100644
index 0000000..ca6f37b
--- /dev/null
+++ b/ui/src/app/entity/entity-autocomplete.tpl.html
@@ -0,0 +1,45 @@
+<!--
+
+    Copyright © 2016-2017 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="tbRequired"
+                 ng-disabled="disabled"
+                 md-no-cache="true"
+                 md-input-name="entity"
+                 ng-model="entity"
+                 md-selected-item="entity"
+                 md-search-text="entitySearchText"
+                 md-search-text-change="entitySearchTextChanged()"
+                 md-items="item in fetchEntities(entitySearchText)"
+                 md-item-text="item.name"
+                 md-min-length="0"
+                 md-floating-label="{{ entityText | translate }}"
+                 md-select-on-match="true"
+                 md-menu-class="tb-entity-autocomplete">
+    <md-item-template>
+        <div class="tb-entity-item">
+            <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
+        </div>
+    </md-item-template>
+    <md-not-found>
+        <div class="tb-not-found">
+            <span translate translate-values='{ entity: entitySearchText }'>{{ noEntitiesMatchingText }}</span>
+        </div>
+    </md-not-found>
+    <div ng-messages="theForm.entity.$error">
+        <div translate ng-message="required">{{ entityRequiredText }}</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js
index 777dbb8..b1d92c6 100644
--- a/ui/src/app/entity/entity-filter.directive.js
+++ b/ui/src/app/entity/entity-filter.directive.js
@@ -32,14 +32,6 @@ export default function EntityFilterDirective($compile, $templateCache, $q, enti
 
         scope.ngModelCtrl = ngModelCtrl;
 
-        scope.itemName = function(item) {
-            if (item) {
-                return entityService.entityName(scope.entityType, item);
-            } else {
-                return '';
-            }
-        }
-
         scope.fetchEntities = function(searchText, limit) {
             var deferred = $q.defer();
             entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit).then(function success(result) {
diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html
index 508e4ba..cbb997e 100644
--- a/ui/src/app/entity/entity-filter.tpl.html
+++ b/ui/src/app/entity/entity-filter.tpl.html
@@ -33,7 +33,7 @@
                         md-min-length="0"
                         placeholder="{{ 'entity.entity-list' | translate }}">
                         <md-item-template>
-                            <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{itemName(item)}}</span>
+                            <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
                         </md-item-template>
                         <md-not-found>
                             <span translate translate-values='{ entity: entitySearchText }'>entity.no-entities-matching</span>
diff --git a/ui/src/app/entity/entity-select.directive.js b/ui/src/app/entity/entity-select.directive.js
new file mode 100644
index 0000000..8988aae
--- /dev/null
+++ b/ui/src/app/entity/entity-select.directive.js
@@ -0,0 +1,86 @@
+/*
+ * Copyright © 2016-2017 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 './entity-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySelectTemplate from './entity-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySelect($compile, $templateCache) {
+
+    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 = null;
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                var value = ngModelCtrl.$viewValue;
+                if (scope.model && scope.model.entityType && scope.model.entityId) {
+                    if (!value) {
+                        value = {};
+                    }
+                    value.entityType = scope.model.entityType;
+                    value.id = scope.model.entityId;
+                    ngModelCtrl.$setViewValue(value);
+                } else {
+                    ngModelCtrl.$setViewValue(null);
+                }
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                scope.model = {};
+                scope.model.entityType = value.entityType;
+                scope.model.entityId = value.id;
+            } else {
+                scope.model = null;
+            }
+        }
+
+        scope.$watch('model.entityType', function () {
+            scope.updateView();
+        });
+
+        scope.$watch('model.entityId', function () {
+            scope.updateView();
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled'
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-select.scss b/ui/src/app/entity/entity-select.scss
new file mode 100644
index 0000000..166c519
--- /dev/null
+++ b/ui/src/app/entity/entity-select.scss
@@ -0,0 +1,19 @@
+/**
+ * Copyright © 2016-2017 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-entity-select {
+
+}
\ No newline at end of file
diff --git a/ui/src/app/entity/entity-select.tpl.html b/ui/src/app/entity/entity-select.tpl.html
new file mode 100644
index 0000000..a354b1d
--- /dev/null
+++ b/ui/src/app/entity/entity-select.tpl.html
@@ -0,0 +1,29 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div layout='row' class="tb-entity-select">
+    <tb-entity-type-select style="min-width: 100px;"
+                           ng-model="model.entityType">
+    </tb-entity-type-select>
+    <tb-entity-autocomplete flex
+                            the-form="theForm"
+                            ng-disabled="disabled"
+                            tb-required="tbRequired"
+                            entity-type="model.entityType"
+                            ng-model="model.entityId">
+    </tb-entity-autocomplete>
+</div>
\ No newline at end of file
diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js
index 6a5a045..3a595a3 100644
--- a/ui/src/app/entity/entity-type-select.directive.js
+++ b/ui/src/app/entity/entity-type-select.directive.js
@@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function EntityTypeSelect($compile, $templateCache, userService, types) {
+export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) {
 
     var linker = function (scope, element, attrs, ngModelCtrl) {
         var template = $templateCache.get(entityTypeSelectTemplate);
@@ -51,10 +51,12 @@ export default function EntityTypeSelect($compile, $templateCache, userService, 
                 scope.entityTypes.customer = types.entityType.customer;
                 scope.entityTypes.rule = types.entityType.rule;
                 scope.entityTypes.plugin = types.entityType.plugin;
+                scope.entityTypes.dashboard = types.entityType.dashboard;
                 break;
             case 'CUSTOMER_USER':
                 scope.entityTypes.device = types.entityType.device;
                 scope.entityTypes.asset = types.entityType.asset;
+                scope.entityTypes.dashboard = types.entityType.dashboard;
                 break;
         }
 
@@ -67,20 +69,7 @@ export default function EntityTypeSelect($compile, $templateCache, userService, 
         }
 
         scope.typeName = function(type) {
-            switch (type) {
-                case types.entityType.device:
-                    return 'entity.type-device';
-                case types.entityType.asset:
-                    return 'entity.type-asset';
-                case types.entityType.rule:
-                    return 'entity.type-rule';
-                case types.entityType.plugin:
-                    return 'entity.type-plugin';
-                case types.entityType.tenant:
-                    return 'entity.type-tenant';
-                case types.entityType.customer:
-                    return 'entity.type-customer';
-            }
+            return utils.entityTypeName(type);
         }
 
         scope.updateValidity = function () {
diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js
index 8423f11..bed9562 100644
--- a/ui/src/app/entity/index.js
+++ b/ui/src/app/entity/index.js
@@ -18,12 +18,15 @@ import EntityAliasesController from './entity-aliases.controller';
 import EntityTypeSelectDirective from './entity-type-select.directive';
 import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
 import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
+import EntityAutocompleteDirective from './entity-autocomplete.directive';
+import EntitySelectDirective from './entity-select.directive';
 import EntityFilterDirective from './entity-filter.directive';
 import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';
 import AliasesEntitySelectDirective from './aliases-entity-select.directive';
 import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
 import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
 import AttributeTableDirective from './attribute/attribute-table.directive';
+import RelationTableDirective from './relation/relation-table.directive';
 
 export default angular.module('thingsboard.entity', [])
     .controller('EntityAliasesController', EntityAliasesController)
@@ -33,7 +36,10 @@ export default angular.module('thingsboard.entity', [])
     .directive('tbEntityTypeSelect', EntityTypeSelectDirective)
     .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
     .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
+    .directive('tbEntityAutocomplete', EntityAutocompleteDirective)
+    .directive('tbEntitySelect', EntitySelectDirective)
     .directive('tbEntityFilter', EntityFilterDirective)
     .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
     .directive('tbAttributeTable', AttributeTableDirective)
+    .directive('tbRelationTable', RelationTableDirective)
     .name;
diff --git a/ui/src/app/entity/relation/add-relation-dialog.controller.js b/ui/src/app/entity/relation/add-relation-dialog.controller.js
new file mode 100644
index 0000000..c331946
--- /dev/null
+++ b/ui/src/app/entity/relation/add-relation-dialog.controller.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2016-2017 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.
+ */
+/*@ngInject*/
+export default function AddRelationDialogController($scope, $mdDialog, types, entityRelationService, from) {
+
+    var vm = this;
+
+    vm.types = types;
+
+    vm.relation = {};
+    vm.relation.from = from;
+    vm.relation.type = types.entityRelationType.contains;
+
+    vm.add = add;
+    vm.cancel = cancel;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function add() {
+        $scope.theForm.$setPristine();
+        entityRelationService.saveRelation(vm.relation).then(
+            function success() {
+                $mdDialog.hide();
+            }
+        );
+    }
+
+}
diff --git a/ui/src/app/entity/relation/add-relation-dialog.tpl.html b/ui/src/app/entity/relation/add-relation-dialog.tpl.html
new file mode 100644
index 0000000..b32d23a
--- /dev/null
+++ b/ui/src/app/entity/relation/add-relation-dialog.tpl.html
@@ -0,0 +1,64 @@
+<!--
+
+    Copyright © 2016-2017 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-dialog aria-label="{{ 'relation.add' | translate }}" style="min-width: 400px;">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>relation.add</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <fieldset ng-disabled="loading">
+                        <md-input-container class="md-block">
+                            <label translate>relation.relation-type</label>
+                            <md-select required ng-model="vm.relation.type" ng-disabled="loading">
+                                <md-option ng-repeat="type in vm.types.entityRelationType" ng-value="type">
+                                    <span>{{('relation.relation-types.' + type) | translate}}</span>
+                                </md-option>
+                            </md-select>
+                        </md-input-container>
+                        <span class="tb-small">{{'entity.entity' | translate }}</span>
+                        <tb-entity-select flex
+                            the-form="theForm"
+                            tb-required="true"
+                            ng-model="vm.relation.to">
+                        </tb-entity-select>
+                    </fieldset>
+                </md-content>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/entity/relation/relation-table.directive.js b/ui/src/app/entity/relation/relation-table.directive.js
new file mode 100644
index 0000000..bf4aa52
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-table.directive.js
@@ -0,0 +1,179 @@
+/*
+ * Copyright © 2016-2017 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 'angular-material-data-table/dist/md-data-table.min.css';
+import './relation-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import relationTableTemplate from './relation-table.tpl.html';
+import addRelationTemplate from './add-relation-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import AddRelationController from './add-relation-dialog.controller';
+
+/*@ngInject*/
+export default function RelationTable() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            entityId: '=',
+            entityType: '@'
+        },
+        controller: RelationTableController,
+        controllerAs: 'vm',
+        templateUrl: relationTableTemplate
+    };
+}
+
+/*@ngInject*/
+function RelationTableController($scope, $q, $mdDialog, $document, $translate, $filter, utils, types, entityRelationService) {
+
+    let vm = this;
+
+    vm.relations = [];
+    vm.relationsCount = 0;
+    vm.allRelations = [];
+    vm.selectedRelations = [];
+
+    vm.query = {
+        order: 'typeName',
+        limit: 5,
+        page: 1,
+        search: null
+    };
+
+    vm.enterFilterMode = enterFilterMode;
+    vm.exitFilterMode = exitFilterMode;
+    vm.onReorder = onReorder;
+    vm.onPaginate = onPaginate;
+    vm.addRelation = addRelation;
+    vm.editRelation = editRelation;
+    vm.deleteRelation = deleteRelation;
+    vm.deleteRelations = deleteRelations;
+    vm.reloadRelations = reloadRelations;
+    vm.updateRelations = updateRelations;
+
+
+    $scope.$watch("vm.entityId", function(newVal, prevVal) {
+        if (newVal && !angular.equals(newVal, prevVal)) {
+            reloadRelations();
+        }
+    });
+
+    $scope.$watch("vm.query.search", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+            updateRelations();
+        }
+    });
+
+    function enterFilterMode () {
+        vm.query.search = '';
+    }
+
+    function exitFilterMode () {
+        vm.query.search = null;
+        updateRelations();
+    }
+
+    function onReorder () {
+        updateRelations();
+    }
+
+    function onPaginate () {
+        updateRelations();
+    }
+
+    function addRelation($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var from = {
+            id: vm.entityId,
+            entityType: vm.entityType
+        };
+        $mdDialog.show({
+            controller: AddRelationController,
+            controllerAs: 'vm',
+            templateUrl: addRelationTemplate,
+            parent: angular.element($document[0].body),
+            locals: { from: from },
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function () {
+            reloadRelations();
+        }, function () {
+        });
+    }
+
+    function editRelation($event, /*relation*/) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        //TODO:
+    }
+
+    function deleteRelation($event, /*relation*/) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        //TODO:
+    }
+
+    function deleteRelations($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        //TODO:
+    }
+
+    function reloadRelations () {
+        vm.allRelations.length = 0;
+        vm.relations.length = 0;
+        vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType);
+        vm.relationsPromise.then(
+            function success(allRelations) {
+                allRelations.forEach(function(relation) {
+                    relation.typeName = $translate.instant('relation.relation-type.' + relation.type);
+                    relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
+                });
+                vm.allRelations = allRelations;
+                vm.selectedRelations = [];
+                vm.updateRelations();
+                vm.relationsPromise = null;
+            },
+            function fail() {
+                vm.allRelations = [];
+                vm.selectedRelations = [];
+                vm.updateRelations();
+                vm.relationsPromise = null;
+            }
+        )
+    }
+
+    function updateRelations () {
+        vm.selectedRelations = [];
+        var result = $filter('orderBy')(vm.allRelations, vm.query.order);
+        if (vm.query.search != null) {
+            result = $filter('filter')(result, {$: vm.query.search});
+        }
+        vm.relationsCount = result.length;
+        var startIndex = vm.query.limit * (vm.query.page - 1);
+        vm.relations = result.slice(startIndex, startIndex + vm.query.limit);
+    }
+
+}
diff --git a/ui/src/app/entity/relation/relation-table.scss b/ui/src/app/entity/relation/relation-table.scss
new file mode 100644
index 0000000..f5df1fc
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-table.scss
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2017 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 '../../../scss/constants';
+
+$md-light: rgba(255, 255, 255, 100%);
+
+.tb-relation-table {
+  md-toolbar.md-table-toolbar.alternate {
+    .md-toolbar-tools {
+      md-icon {
+        color: $md-light;
+      }
+    }
+  }
+}
diff --git a/ui/src/app/entity/relation/relation-table.tpl.html b/ui/src/app/entity/relation/relation-table.tpl.html
new file mode 100644
index 0000000..dc0df57
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-table.tpl.html
@@ -0,0 +1,119 @@
+<!--
+
+    Copyright © 2016-2017 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-content flex class="md-padding tb-absolute-fill tb-relation-table tb-data-table" layout="column">
+    <div layout="column" class="md-whiteframe-z1">
+        <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
+                                                                 && vm.query.search === null">
+            <div class="md-toolbar-tools">
+                <span translate>relation.entity-relations</span>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.addRelation($event)">
+                    <md-icon>add</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.add' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+                    <md-icon>search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.reloadRelations()">
+                    <md-icon>refresh</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.refresh' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
+                                                                 && vm.query.search != null">
+            <div class="md-toolbar-tools">
+                <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+                    <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-input-container flex>
+                    <label>&nbsp;</label>
+                    <input ng-model="vm.query.search" 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>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.close' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedRelations.length">
+            <div class="md-toolbar-tools">
+                <span translate
+                      translate-values="{count: selectedRelations.length}"
+                      translate-interpolation="messageformat">relation.selected-relations</span>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.deleteRelations($event)">
+                    <md-icon>delete</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.delete' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-table-container>
+            <table md-table md-row-select multiple="" ng-model="vm.selectedRelations" md-progress="vm.relationsDeferred.promise">
+                <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+                <tr md-row>
+                    <th md-column md-order-by="typeName"><span translate>relation.type</span></th>
+                    <th md-column md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th>
+                    <th md-column md-order-by="toName"><span translate>relation.to-entity-name</span></th>
+                    <th md-column><span>&nbsp</span></th>
+                </tr>
+                </thead>
+                <tbody md-body>
+                <tr md-row md-select="relation" md-select-id="relation" md-auto-select ng-repeat="relation in vm.relations">
+                    <td md-cell>{{ relation.typeName }}</td>
+                    <td md-cell>{{ relation.toEntityTypeName }}</td>
+                    <td md-cell>{{ relation.toName }}</td>
+                    <td md-cell class="tb-action-cell">
+                        <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
+                                   ng-click="vm.editRelation($event, relation)">
+                            <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'relation.edit' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                        <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteRelation($event, relation)">
+                            <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'relation.delete' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </md-table-container>
+        <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
+                             md-page="vm.query.page" md-total="{{vm.relationsCount}}"
+                             md-on-paginate="onPaginate" md-page-select>
+        </md-table-pagination>
+    </div>
+</md-content>
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 50f6d8f..d25db5d 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -106,6 +106,12 @@ export default angular.module('thingsboard.locale', [])
                     "enable-tls": "Enable TLS",
                     "send-test-mail": "Send test mail"
                 },
+                "alarm": {
+                    "alarm": "Alarm",
+                    "select-alarm": "Select alarm",
+                    "no-alarms-matching": "No alarms matching '{{entity}}' were found.",
+                    "alarm-required": "Alarm is required"
+                },
                 "asset": {
                     "asset": "Asset",
                     "assets": "Assets",
@@ -157,7 +163,10 @@ export default angular.module('thingsboard.locale', [])
                     "unassign-assets-title": "Are you sure you want to unassign { count, select, 1 {1 asset} other {# assets} }?",
                     "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.",
                     "copyId": "Copy asset Id",
-                    "idCopiedMessage": "Asset Id has been copied to clipboard"
+                    "idCopiedMessage": "Asset Id has been copied to clipboard",
+                    "select-asset": "Select asset",
+                    "no-assets-matching": "No assets matching '{{entity}}' were found.",
+                    "asset-required": "Asset is required"
                 },
                 "attribute": {
                     "attributes": "Attributes",
@@ -169,6 +178,7 @@ export default angular.module('thingsboard.locale', [])
                     "scope-shared": "Shared attributes",
                     "add": "Add attribute",
                     "key": "Key",
+                    "last-update-time": "Last update time",
                     "key-required": "Attribute key is required.",
                     "value": "Value",
                     "value-required": "Attribute value is required.",
@@ -210,6 +220,7 @@ export default angular.module('thingsboard.locale', [])
                     "enter-search": "Enter search"
                 },
                 "customer": {
+                    "customer": "Customer",
                     "customers": "Customers",
                     "management": "Customer management",
                     "dashboard": "Customer Dashboard",
@@ -246,7 +257,10 @@ export default angular.module('thingsboard.locale', [])
                     "details": "Details",
                     "events": "Events",
                     "copyId": "Copy customer Id",
-                    "idCopiedMessage": "Customer Id has been copied to clipboard"
+                    "idCopiedMessage": "Customer Id has been copied to clipboard",
+                    "select-customer": "Select customer",
+                    "no-customers-matching": "No customers matching '{{entity}}' were found.",
+                    "customer-required": "Customer is required"
                 },
                 "datetime": {
                     "date-from": "Date from",
@@ -304,7 +318,7 @@ export default angular.module('thingsboard.locale', [])
                     "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
                     "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
                     "select-dashboard": "Select dashboard",
-                    "no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.",
+                    "no-dashboards-matching": "No dashboards matching '{{entity}}' were found.",
                     "dashboard-required": "Dashboard is required.",
                     "select-existing": "Select existing dashboard",
                     "create-new": "Create new dashboard",
@@ -425,7 +439,7 @@ export default angular.module('thingsboard.locale', [])
                     "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": "Configure '{{alias}}' alias",
-                    "no-devices-matching": "No devices matching '{{device}}' were found.",
+                    "no-devices-matching": "No devices matching '{{entity}}' were found.",
                     "alias": "Alias",
                     "alias-required": "Device alias is required.",
                     "remove-alias": "Remove device alias",
@@ -497,7 +511,8 @@ export default angular.module('thingsboard.locale', [])
                     "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
                     "is-gateway": "Is gateway",
                     "public": "Public",
-                    "device-public": "Device is public"
+                    "device-public": "Device is public",
+                    "select-device": "Select device"
                 },
                 "dialog": {
                     "close": "Close dialog"
@@ -535,6 +550,9 @@ export default angular.module('thingsboard.locale', [])
                     "type-plugin": "Plugin",
                     "type-tenant": "Tenant",
                     "type-customer": "Customer",
+                    "type-user": "User",
+                    "type-dashboard": "Dashboard",
+                    "type-alarm": "Alarm",
                     "select-entities": "Select entities",
                     "no-aliases-found": "No aliases found.",
                     "no-alias-matching": "'{{alias}}' not found.",
@@ -672,7 +690,7 @@ export default angular.module('thingsboard.locale', [])
                     "system": "System",
                     "select-plugin": "Select plugin",
                     "plugin": "Plugin",
-                    "no-plugins-matching": "No plugins matching '{{plugin}}' were found.",
+                    "no-plugins-matching": "No plugins matching '{{entity}}' were found.",
                     "plugin-required": "Plugin is required.",
                     "plugin-require-match": "Please select an existing plugin.",
                     "events": "Events",
@@ -685,6 +703,7 @@ export default angular.module('thingsboard.locale', [])
                     "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
                     "copyId": "Copy plugin Id",
                     "idCopiedMessage": "Plugin Id has been copied to clipboard"
+
                 },
                 "position": {
                     "top": "Top",
@@ -697,7 +716,24 @@ export default angular.module('thingsboard.locale', [])
                     "change-password": "Change Password",
                     "current-password": "Current password"
                 },
+                "relation": {
+                    "relations": "Relations",
+                    "entity-relations": "Entity relations",
+                    "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
+                    "type": "Type",
+                    "to-entity-type": "Entity type",
+                    "to-entity-name": "Entity name",
+                    "edit": "Edit relation",
+                    "delete": "Delete relation",
+                    "relation-type": "Relation type",
+                    "relation-types": {
+                        "Contains": "Contains",
+                        "Manages": "Manages"
+                    },
+                    "add": "Add relation"
+                },
                 "rule": {
+                    "rule": "Rule",
                     "rules": "Rules",
                     "delete": "Delete rule",
                     "activate": "Activate rule",
@@ -749,12 +785,16 @@ export default angular.module('thingsboard.locale', [])
                     "rule-file": "Rule file",
                     "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.",
                     "copyId": "Copy rule Id",
-                    "idCopiedMessage": "Rule Id has been copied to clipboard"
+                    "idCopiedMessage": "Rule Id has been copied to clipboard",
+                    "select-rule": "Select rule",
+                    "no-rules-matching": "No rules matching '{{entity}}' were found.",
+                    "rule-required": "Rule is required"
                 },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
                 },
                 "tenant": {
+                    "tenant": "Tenant",
                     "tenants": "Tenants",
                     "management": "Tenant management",
                     "add": "Add Tenant",
@@ -775,7 +815,10 @@ export default angular.module('thingsboard.locale', [])
                     "details": "Details",
                     "events": "Events",
                     "copyId": "Copy tenant Id",
-                    "idCopiedMessage": "Tenant Id has been copied to clipboard"
+                    "idCopiedMessage": "Tenant Id has been copied to clipboard",
+                    "select-tenant": "Select tenant",
+                    "no-tenants-matching": "No tenants matching '{{entity}}' were found.",
+                    "tenant-required": "Tenant is required"
                 },
                 "timeinterval": {
                     "seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }",
@@ -803,6 +846,7 @@ export default angular.module('thingsboard.locale', [])
                     "time-period": "Time period"
                 },
                 "user": {
+                    "user": "User",
                     "users": "Users",
                     "customer-users": "Customer Users",
                     "tenant-admins": "Tenant Admins",
@@ -828,7 +872,10 @@ export default angular.module('thingsboard.locale', [])
                     "last-name": "Last Name",
                     "description": "Description",
                     "default-dashboard": "Default dashboard",
-                    "always-fullscreen": "Always fullscreen"
+                    "always-fullscreen": "Always fullscreen",
+                    "select-user": "Select user",
+                    "no-users-matching": "No users matching '{{entity}}' were found.",
+                    "user-required": "User is required"
                 },
                 "value": {
                     "type": "Value type",
diff --git a/ui/src/app/locale/locale.constant-es.js b/ui/src/app/locale/locale.constant-es.js
index 153019d..e4d4b49 100644
--- a/ui/src/app/locale/locale.constant-es.js
+++ b/ui/src/app/locale/locale.constant-es.js
@@ -235,7 +235,7 @@
               "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
               "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
               "select-dashboard": "Seleccionar panel",
-              "no-dashboards-matching": "Panel '{{dashboard}}' no encontrado.",
+              "no-dashboards-matching": "Panel '{{entity}}' no encontrado.",
               "dashboard-required": "Panel requerido.",
               "select-existing": "Seleccionar paneles existentes",
               "create-new": "Crear nuevo panel",
@@ -330,7 +330,7 @@
               "create-new-key": "Crear nueva clave!",
               "duplicate-alias-error": "Alias duplicado '{{alias}}'.<br> El alias de los dispositivos deben ser únicos dentro del panel.",
               "configure-alias": "Configurar alias '{{alias}}'",
-              "no-devices-matching": "No se encontró dispositivo '{{device}}'",
+              "no-devices-matching": "No se encontró dispositivo '{{entity}}'",
               "alias": "Alias",
               "alias-required": "Alias de dispositivo requerido.",
               "remove-alias": "Eliminar alias",
@@ -529,7 +529,7 @@
               "system": "Sistema",
               "select-plugin": "plugin",
               "plugin": "Plugin",
-              "no-plugins-matching": "No se encontraron plugins: '{{plugin}}'",
+              "no-plugins-matching": "No se encontraron plugins: '{{entity}}'",
               "plugin-required": "Plugin requerido.",
               "plugin-require-match": "Por favor, elija un plugin existente.",
               "events": "Eventos",
diff --git a/ui/src/app/locale/locale.constant-ko.js b/ui/src/app/locale/locale.constant-ko.js
index 8b2d4ba..32dbf49 100644
--- a/ui/src/app/locale/locale.constant-ko.js
+++ b/ui/src/app/locale/locale.constant-ko.js
@@ -218,7 +218,7 @@ export default function addLocaleKorean(locales) {
             "unassign-dashboards-title": "{ count, select, 1 {대시보드 1개} other {대시보드 #개} }의 할당을 취소하시겠습니까?",
             "unassign-dashboards-text": "선택된 대시보드가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.",
             "select-dashboard": "대시보드 선택",
-            "no-dashboards-matching": "'{{dashboard}}'와 일치하는 대시보드가 없습니다.",
+            "no-dashboards-matching": "'{{entity}}'와 일치하는 대시보드가 없습니다.",
             "dashboard-required": "대시보드를 입력하세요.",
             "select-existing": "기존 대시보드 선택",
             "create-new": "대시보드 생성",
@@ -305,7 +305,7 @@ export default function addLocaleKorean(locales) {
             "create-new-key": "새로 만들기!",
             "duplicate-alias-error": "중복된 '{{alias}}' 앨리어스가 있습니다.<br> 디바이스 앨리어스는 대시보드 내에서 고유해야 합니다.",
             "configure-alias": "'{{alias}}' 앨리어스 구성",
-            "no-devices-matching": "'{{device}}'와 일치하는 디바이스를 찾을 수 없습니다.",
+            "no-devices-matching": "'{{entity}}'와 일치하는 디바이스를 찾을 수 없습니다.",
             "alias": "앨리어스",
             "alias-required": "디바이스 앨리어스를 입력하세요.",
             "remove-alias": "디바이스 앨리어스 삭제",
@@ -496,7 +496,7 @@ export default function addLocaleKorean(locales) {
             "system": "시스템",
             "select-plugin": "플러그인 선택",
             "plugin": "플러그인",
-            "no-plugins-matching": "'{{plugin}}'과 일치하는 플러그인을 찾을 수 없습니다.",
+            "no-plugins-matching": "'{{entity}}'과 일치하는 플러그인을 찾을 수 없습니다.",
             "plugin-required": "플러그인을 입력하세요.",
             "plugin-require-match": "기존의 플러그인을 선택해주세요.",
             "events": "이벤트",
diff --git a/ui/src/app/locale/locale.constant-ru.js b/ui/src/app/locale/locale.constant-ru.js
index 47c9460..d7734b4 100644
--- a/ui/src/app/locale/locale.constant-ru.js
+++ b/ui/src/app/locale/locale.constant-ru.js
@@ -235,7 +235,7 @@ export default function addLocaleRussian(locales) {
             "socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard",
             "socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard",
             "select-dashboard": "Выберите дашборд",
-            "no-dashboards-matching": "Дашборд '{{dashboard}}' не найден.",
+            "no-dashboards-matching": "Дашборд '{{entity}}' не найден.",
             "dashboard-required": "Дашборд обязателен.",
             "select-existing": "Выберите существующий дашборд",
             "create-new": "Создать новый дашборд",
@@ -330,7 +330,7 @@ export default function addLocaleRussian(locales) {
             "create-new-key": "Создать новый!",
             "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.<br>В рамках дашборда псевдонимы устройств должны быть уникальными.",
             "configure-alias": "Конфигурировать '{{alias}}' псевдоним",
-            "no-devices-matching": "Устройство '{{device}}' не найдено.",
+            "no-devices-matching": "Устройство '{{entity}}' не найдено.",
             "alias": "Псевдоним",
             "alias-required": "Псевдоним устройства обязателен.",
             "remove-alias": "Удалить псевдоним устройства",
@@ -529,7 +529,7 @@ export default function addLocaleRussian(locales) {
             "system": "Системный",
             "select-plugin": "Выберите плагин",
             "plugin": "Плагин",
-            "no-plugins-matching": "Плагин '{{plugin}}' не найден.",
+            "no-plugins-matching": "Плагин '{{entity}}' не найден.",
             "plugin-required": "Плагин обязателен.",
             "plugin-require-match": "Пожалуйста, выберите существующий плагин.",
             "events": "События",
diff --git a/ui/src/app/locale/locale.constant-zh.js b/ui/src/app/locale/locale.constant-zh.js
index ba8e3c0..3246070 100644
--- a/ui/src/app/locale/locale.constant-zh.js
+++ b/ui/src/app/locale/locale.constant-zh.js
@@ -235,7 +235,7 @@ export default function addLocaleChinese(locales) {
             "socialshare-text" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
             "socialshare-title" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
             "select-dashboard" : "选择仪表板",
-            "no-dashboards-matching" : "找不到符合 '{{dashboard}}' 的仪表板。",
+            "no-dashboards-matching" : "找不到符合 '{{entity}}' 的仪表板。",
             "dashboard-required" : "仪表板是必需的。",
             "select-existing" : "选择现有仪表板",
             "create-new" : "创建新的仪表板",
@@ -330,7 +330,7 @@ export default function addLocaleChinese(locales) {
             "create-new-key": "创建一个新的!",
             "duplicate-alias-error" : "找到重复别名 '{{alias}}'。 <br> 设备别名必须是唯一的。",
             "configure-alias" : "配置 '{{alias}}' 别名",
-            "no-devices-matching" : "找不到与 '{{device}}' 匹配的设备。",
+            "no-devices-matching" : "找不到与 '{{entity}}' 匹配的设备。",
             "alias" : "别名",
             "alias-required" : "需要设备别名。",
             "remove-alias": "删除设备别名",
@@ -529,7 +529,7 @@ export default function addLocaleChinese(locales) {
             "system" : "系统",
             "select-plugin" : "选择插件",
             "plugin" : "插件",
-            "no-plugins-matching" : "没有找到匹配'{{plugin}}'的插件。",
+            "no-plugins-matching" : "没有找到匹配'{{entity}}'的插件。",
             "plugin-required" : "插件是必需的。",
             "plugin-require-match" : "请选择一个现有的插件。",
             "events" : "事件",
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 4ffabc9..c062d4e 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -261,6 +261,45 @@ pre.tb-highlight {
   font-size: 16px;
 }
 
+.tb-data-table {
+  md-toolbar {
+    z-index: 0;
+  }
+  span.no-data-found {
+    position: relative;
+    height: calc(100% - 57px);
+    text-transform: uppercase;
+    display: flex;
+  }
+  table.md-table {
+    tbody {
+      tr {
+        td {
+          &.tb-action-cell {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            min-width: 72px;
+            max-width: 72px;
+            width: 72px;
+            .md-button {
+              &.md-icon-button {
+                margin: 0;
+                padding: 6px;
+                width: 36px;
+                height: 36px;
+              }
+            }
+            .tb-spacer {
+              padding-left: 38px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
 
 /***********************
  * Flow