thingsboard-memoizeit

Merge remote-tracking branch 'upstream/dao-refactoring-vs'

5/30/2017 3:30:43 AM

Changes

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

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

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)

pom.xml 4(+2 -2)

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

ui/package.json 2(+1 -1)

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

ui/src/app/components/datasource-device.directive.js 248(+0 -248)

ui/src/app/components/datasource-device.tpl.html 137(+0 -137)

ui/src/app/components/device-alias-select.directive.js 144(+0 -144)

ui/src/app/components/device-filter.directive.js 217(+0 -217)

ui/src/app/components/device-filter.tpl.html 67(+0 -67)

ui/src/app/dashboard/aliases-device-select.directive.js 153(+0 -153)

ui/src/app/dashboard/device-aliases.controller.js 227(+0 -227)

ui/src/app/device/attribute/attribute-table.directive.js 421(+0 -421)

ui/src/app/device/attribute/attribute-table.tpl.html 210(+0 -210)

ui/src/app/device/attribute/edit-attribute-value.controller.js 71(+0 -71)

ui/src/app/device/attribute/edit-attribute-value.tpl.html 72(+0 -72)

Details

diff --git a/application/pom.xml b/application/pom.xml
index f3fd1d0..9364cfc 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
new file mode 100644
index 0000000..7d29d57
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -0,0 +1,129 @@
+/**
+ * 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.controller;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.asset.AssetSearchQuery;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api")
+public class AlarmController extends BaseController {
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET)
+    @ResponseBody
+    public Alarm getAlarmById(@PathVariable("alarmId") String strAlarmId) throws ThingsboardException {
+        checkParameter("alarmId", strAlarmId);
+        try {
+            AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
+            return checkAlarmId(alarmId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm", method = RequestMethod.POST)
+    @ResponseBody
+    public Alarm saveAlarm(@RequestBody Alarm alarm) throws ThingsboardException {
+        try {
+            alarm.setTenantId(getCurrentUser().getTenantId());
+            return checkNotNull(alarmService.createOrUpdateAlarm(alarm));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST)
+    @ResponseStatus(value = HttpStatus.OK)
+    public void ackAlarm(@PathVariable("alarmId") String strAlarmId) throws ThingsboardException {
+        checkParameter("alarmId", strAlarmId);
+        try {
+            AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
+            checkAlarmId(alarmId);
+            alarmService.ackAlarm(alarmId, System.currentTimeMillis()).get();
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST)
+    @ResponseStatus(value = HttpStatus.OK)
+    public void clearAlarm(@PathVariable("alarmId") String strAlarmId) throws ThingsboardException {
+        checkParameter("alarmId", strAlarmId);
+        try {
+            AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
+            checkAlarmId(alarmId);
+            alarmService.clearAlarm(alarmId, System.currentTimeMillis()).get();
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
+    @ResponseBody
+    public TimePageData<Alarm> getAlarms(
+            @PathVariable("entityType") String strEntityType,
+            @PathVariable("entityId") String strEntityId,
+            @RequestParam(required = false) String status,
+            @RequestParam int limit,
+            @RequestParam(required = false) Long startTime,
+            @RequestParam(required = false) Long endTime,
+            @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+            @RequestParam(required = false) String offset
+    ) throws ThingsboardException {
+        checkParameter("EntityId", strEntityId);
+        checkParameter("EntityType", strEntityType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
+        AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
+        checkEntityId(entityId);
+        try {
+            TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+            return checkNotNull(alarmService.findAlarms(new AlarmQuery(entityId, pageLink, alarmStatus)).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 24497d4..dd43e1c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -21,6 +21,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -136,13 +137,18 @@ public class AssetController extends BaseController {
     @ResponseBody
     public TextPageData<Asset> getTenantAssets(
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
         try {
             TenantId tenantId = getCurrentUser().getTenantId();
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink));
+            } else {
+                return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -167,6 +173,7 @@ public class AssetController extends BaseController {
     public TextPageData<Asset> getCustomerAssets(
             @PathVariable("customerId") String strCustomerId,
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
@@ -176,7 +183,11 @@ public class AssetController extends BaseController {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
             checkCustomerId(customerId);
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
+            } else {
+                return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -231,4 +242,18 @@ public class AssetController extends BaseController {
             throw handleException(e);
         }
     }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/asset/types", method = RequestMethod.GET)
+    @ResponseBody
+    public List<TenantAssetType> getAssetTypes() throws ThingsboardException {
+        try {
+            SecurityUser user = getCurrentUser();
+            TenantId tenantId = user.getTenantId();
+            ListenableFuture<List<TenantAssetType>> assetTypes = assetService.findAssetTypesByTenantId(tenantId);
+            return checkNotNull(assetTypes.get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 12d43e0..1feef4a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -23,6 +23,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.thingsboard.server.actors.service.ActorService;
 import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -34,6 +36,7 @@ import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
+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;
@@ -82,6 +85,9 @@ public abstract class BaseController {
     protected AssetService assetService;
 
     @Autowired
+    protected AlarmService alarmService;
+
+    @Autowired
     protected DeviceCredentialsService deviceCredentialsService;
 
     @Autowired
@@ -305,7 +311,7 @@ public abstract class BaseController {
         }
     }
 
-    private void checkDevice(Device device) throws ThingsboardException {
+    protected void checkDevice(Device device) throws ThingsboardException {
         checkNotNull(device);
         checkTenantId(device.getTenantId());
         if (device.getCustomerId() != null && !device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
@@ -332,6 +338,22 @@ public abstract class BaseController {
         }
     }
 
+    Alarm checkAlarmId(AlarmId alarmId) throws ThingsboardException {
+        try {
+            validateId(alarmId, "Incorrect alarmId " + alarmId);
+            Alarm alarm = alarmService.findAlarmByIdAsync(alarmId).get();
+            checkAlarm(alarm);
+            return alarm;
+        } catch (Exception e) {
+            throw handleException(e, false);
+        }
+    }
+
+    protected void checkAlarm(Alarm alarm) throws ThingsboardException {
+        checkNotNull(alarm);
+        checkTenantId(alarm.getTenantId());
+    }
+
     WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, boolean modify) throws ThingsboardException {
         try {
             validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
@@ -378,14 +400,26 @@ public abstract class BaseController {
         try {
             validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
             Dashboard dashboard = dashboardService.findDashboardById(dashboardId);
-            checkDashboard(dashboard);
+            checkDashboard(dashboard, true);
             return dashboard;
         } catch (Exception e) {
             throw handleException(e, false);
         }
     }
 
-    private void checkDashboard(Dashboard dashboard) throws ThingsboardException {
+    DashboardInfo checkDashboardInfoId(DashboardId dashboardId) throws ThingsboardException {
+        try {
+            validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+            DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(dashboardId);
+            SecurityUser authUser = getCurrentUser();
+            checkDashboard(dashboardInfo, authUser.getAuthority() != Authority.SYS_ADMIN);
+            return dashboardInfo;
+        } catch (Exception e) {
+            throw handleException(e, false);
+        }
+    }
+
+    private void checkDashboard(DashboardInfo dashboard, boolean checkCustomerId) throws ThingsboardException {
         checkNotNull(dashboard);
         checkTenantId(dashboard.getTenantId());
         SecurityUser authUser = getCurrentUser();
@@ -395,7 +429,8 @@ public abstract class BaseController {
                         ThingsboardErrorCode.PERMISSION_DENIED);
             }
         }
-        if (dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+        if (checkCustomerId &&
+                dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
             checkCustomerId(dashboard.getCustomerId());
         }
     }
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 3812610..2a6416c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -41,6 +41,19 @@ public class DashboardController extends BaseController {
         return System.currentTimeMillis();
     }
 
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET)
+    @ResponseBody
+    public DashboardInfo getDashboardInfoById(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
+        checkParameter("dashboardId", strDashboardId);
+        try {
+            DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
+            return checkDashboardInfoId(dashboardId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
     @ResponseBody
@@ -132,6 +145,25 @@ public class DashboardController extends BaseController {
         }
     }
 
+    @PreAuthorize("hasAuthority('SYS_ADMIN')")
+    @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = { "limit" }, method = RequestMethod.GET)
+    @ResponseBody
+    public TextPageData<DashboardInfo> getTenantDashboards(
+            @PathVariable("tenantId") String strTenantId,
+            @RequestParam int limit,
+            @RequestParam(required = false) String textSearch,
+            @RequestParam(required = false) String idOffset,
+            @RequestParam(required = false) String textOffset) throws ThingsboardException {
+        try {
+            TenantId tenantId = new TenantId(toUUID(strTenantId));
+            checkTenantId(tenantId);
+            TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+            return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
     @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
     @ResponseBody
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 38efcb8..8257767 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -21,12 +21,14 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 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.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.device.DeviceSearchQuery;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.exception.ThingsboardException;
@@ -34,6 +36,7 @@ import org.thingsboard.server.service.security.model.SecurityUser;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 @RestController
 @RequestMapping("/api")
@@ -164,13 +167,18 @@ public class DeviceController extends BaseController {
     @ResponseBody
     public TextPageData<Device> getTenantDevices(
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
         try {
             TenantId tenantId = getCurrentUser().getTenantId();
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(deviceService.findDevicesByTenantId(tenantId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(deviceService.findDevicesByTenantIdAndType(tenantId, type, pageLink));
+            } else {
+                return checkNotNull(deviceService.findDevicesByTenantId(tenantId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -195,6 +203,7 @@ public class DeviceController extends BaseController {
     public TextPageData<Device> getCustomerDevices(
             @PathVariable("customerId") String strCustomerId,
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
@@ -204,7 +213,11 @@ public class DeviceController extends BaseController {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
             checkCustomerId(customerId);
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
+            } else {
+                return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -235,4 +248,43 @@ public class DeviceController extends BaseController {
             throw handleException(e);
         }
     }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/devices", method = RequestMethod.POST)
+    @ResponseBody
+    public List<Device> findByQuery(@RequestBody DeviceSearchQuery query) throws ThingsboardException {
+        checkNotNull(query);
+        checkNotNull(query.getParameters());
+        checkNotNull(query.getDeviceTypes());
+        checkEntityId(query.getParameters().getEntityId());
+        try {
+            List<Device> devices = checkNotNull(deviceService.findDevicesByQuery(query).get());
+            devices = devices.stream().filter(device -> {
+                try {
+                    checkDevice(device);
+                    return true;
+                } catch (ThingsboardException e) {
+                    return false;
+                }
+            }).collect(Collectors.toList());
+            return devices;
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/device/types", method = RequestMethod.GET)
+    @ResponseBody
+    public List<TenantDeviceType> getDeviceTypes() throws ThingsboardException {
+        try {
+            SecurityUser user = getCurrentUser();
+            TenantId tenantId = user.getTenantId();
+            ListenableFuture<List<TenantDeviceType>> deviceTypes = deviceService.findDeviceTypesByTenantId(tenantId);
+            return checkNotNull(deviceTypes.get());
+        } catch (Exception e) {
+            throw handleException(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..1a08d56 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,8 @@ 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.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.relation.EntityRelationsQuery;
 import org.thingsboard.server.exception.ThingsboardErrorCode;
 import org.thingsboard.server.exception.ThingsboardException;
@@ -50,19 +52,23 @@ public class EntityRelationController extends BaseController {
     @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {"fromId", "fromType", "relationType", "toId", "toType"})
     @ResponseStatus(value = HttpStatus.OK)
     public void deleteRelation(@RequestParam("fromId") String strFromId,
-                               @RequestParam("fromType") String strFromType, @RequestParam("relationType") String strRelationType,
+                               @RequestParam("fromType") String strFromType,
+                               @RequestParam("relationType") String strRelationType,
+                               @RequestParam("relationTypeGroup") String strRelationTypeGroup,
                                @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
         checkParameter("fromId", strFromId);
         checkParameter("fromType", strFromType);
         checkParameter("relationType", strRelationType);
+        checkParameter("relationTypeGroup", strRelationTypeGroup);
         checkParameter("toId", strToId);
         checkParameter("toType", strToType);
         EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
         EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
         checkEntityId(fromId);
         checkEntityId(toId);
+        RelationTypeGroup relationTypeGroup = RelationTypeGroup.valueOf(strRelationTypeGroup);
         try {
-            Boolean found = relationService.deleteRelation(fromId, toId, strRelationType).get();
+            Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup).get();
             if (!found) {
                 throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
             }
@@ -91,7 +97,9 @@ public class EntityRelationController extends BaseController {
     @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType", "toId", "toType"})
     @ResponseStatus(value = HttpStatus.OK)
     public void checkRelation(@RequestParam("fromId") String strFromId,
-                              @RequestParam("fromType") String strFromType, @RequestParam("relationType") String strRelationType,
+                              @RequestParam("fromType") String strFromType,
+                              @RequestParam("relationType") String strRelationType,
+                              @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
                               @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
         try {
             checkParameter("fromId", strFromId);
@@ -103,7 +111,8 @@ public class EntityRelationController extends BaseController {
             EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
             checkEntityId(fromId);
             checkEntityId(toId);
-            Boolean found = relationService.checkRelation(fromId, toId, strRelationType).get();
+            RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
+            Boolean found = relationService.checkRelation(fromId, toId, strRelationType, typeGroup).get();
             if (!found) {
                 throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
             }
@@ -115,13 +124,34 @@ public class EntityRelationController extends BaseController {
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType"})
     @ResponseBody
-    public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType) throws ThingsboardException {
+    public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
+                                           @RequestParam("fromType") String strFromType,
+                                           @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("fromId", strFromId);
         checkParameter("fromType", strFromType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByFrom(entityId).get());
+            return checkNotNull(relationService.findByFrom(entityId, typeGroup).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @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,
+                                                   @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
+        checkParameter("fromId", strFromId);
+        checkParameter("fromType", strFromType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
+        checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
+        try {
+            return checkNotNull(relationService.findInfoByFrom(entityId, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -130,15 +160,18 @@ public class EntityRelationController extends BaseController {
     @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
-            , @RequestParam("relationType") String strRelationType) throws ThingsboardException {
+    public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
+                                           @RequestParam("fromType") String strFromType,
+                                           @RequestParam("relationType") String strRelationType,
+                                           @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("fromId", strFromId);
         checkParameter("fromType", strFromType);
         checkParameter("relationType", strRelationType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByFromAndType(entityId, strRelationType).get());
+            return checkNotNull(relationService.findByFromAndType(entityId, strRelationType, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -147,13 +180,16 @@ public class EntityRelationController extends BaseController {
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType"})
     @ResponseBody
-    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
+    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
+                                         @RequestParam("toType") String strToType,
+                                         @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("toId", strToId);
         checkParameter("toType", strToType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByTo(entityId).get());
+            return checkNotNull(relationService.findByTo(entityId, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -162,15 +198,18 @@ public class EntityRelationController extends BaseController {
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType", "relationType"})
     @ResponseBody
-    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, @RequestParam("toType") String strToType
-            , @RequestParam("relationType") String strRelationType) throws ThingsboardException {
+    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
+                                         @RequestParam("toType") String strToType,
+                                         @RequestParam("relationType") String strRelationType,
+                                         @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("toId", strToId);
         checkParameter("toType", strToType);
         checkParameter("relationType", strRelationType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByToAndType(entityId, strRelationType).get());
+            return checkNotNull(relationService.findByToAndType(entityId, strRelationType, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -191,4 +230,16 @@ public class EntityRelationController extends BaseController {
         }
     }
 
+    private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
+        RelationTypeGroup result = defaultValue;
+        if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) {
+            try {
+                result = RelationTypeGroup.valueOf(strRelationTypeGroup);
+            } catch (IllegalArgumentException e) {
+                result = defaultValue;
+            }
+        }
+        return result;
+    }
+
 }
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 3bacbfa..f24761c 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/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index b9647e5..435f9a6 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -98,13 +98,15 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
 @IntegrationTest("server.port:0")
 public abstract class AbstractControllerTest {
 
+    protected static final String TEST_TENANT_NAME = "TEST TENANT";
+
     protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
     private static final String SYS_ADMIN_PASSWORD = "sysadmin";
     
-    protected static final String TENANT_ADMIN_EMAIL = "tenant@thingsboard.org";
+    protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org";
     private static final String TENANT_ADMIN_PASSWORD = "tenant";
 
-    protected static final String CUSTOMER_USER_EMAIL = "customer@thingsboard.org";
+    protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";
     private static final String CUSTOMER_USER_PASSWORD = "customer";
     
     protected MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
@@ -147,7 +149,7 @@ public abstract class AbstractControllerTest {
         loginSysAdmin();
 
         Tenant tenant = new Tenant();
-        tenant.setTitle("Tenant");
+        tenant.setTitle(TEST_TENANT_NAME);
         Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
         Assert.assertNotNull(savedTenant);
         tenantId = savedTenant.getId();
diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
new file mode 100644
index 0000000..10a2ff7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
@@ -0,0 +1,659 @@
+/**
+ * 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.controller;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class AssetControllerTest extends AbstractControllerTest {
+
+    private IdComparator<Asset> idComparator = new IdComparator<>();
+
+    private Tenant savedTenant;
+    private User tenantAdmin;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSaveAsset() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        Assert.assertNotNull(savedAsset);
+        Assert.assertNotNull(savedAsset.getId());
+        Assert.assertTrue(savedAsset.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedAsset.getTenantId());
+        Assert.assertNotNull(savedAsset.getCustomerId());
+        Assert.assertEquals(NULL_UUID, savedAsset.getCustomerId().getId());
+        Assert.assertEquals(asset.getName(), savedAsset.getName());
+
+        savedAsset.setName("My new asset");
+        doPost("/api/asset", savedAsset, Asset.class);
+
+        Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(foundAsset.getName(), savedAsset.getName());
+    }
+
+    @Test
+    public void testFindAssetById() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+        Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertNotNull(foundAsset);
+        Assert.assertEquals(savedAsset, foundAsset);
+    }
+
+    @Test
+    public void testFindAssetTypesByTenantId() throws Exception {
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<3;i++) {
+            Asset asset = new Asset();
+            asset.setName("My asset B"+i);
+            asset.setType("typeB");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        for (int i=0;i<7;i++) {
+            Asset asset = new Asset();
+            asset.setName("My asset C"+i);
+            asset.setType("typeC");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        for (int i=0;i<9;i++) {
+            Asset asset = new Asset();
+            asset.setName("My asset A"+i);
+            asset.setType("typeA");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        List<TenantAssetType> assetTypes = doGetTyped("/api/asset/types",
+                new TypeReference<List<TenantAssetType>>(){});
+
+        Assert.assertNotNull(assetTypes);
+        Assert.assertEquals(3, assetTypes.size());
+        Assert.assertEquals("typeA", assetTypes.get(0).getType());
+        Assert.assertEquals("typeB", assetTypes.get(1).getType());
+        Assert.assertEquals("typeC", assetTypes.get(2).getType());
+    }
+
+    @Test
+    public void testDeleteAsset() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        doDelete("/api/asset/"+savedAsset.getId().getId().toString())
+                .andExpect(status().isOk());
+
+        doGet("/api/asset/"+savedAsset.getId().getId().toString())
+                .andExpect(status().isNotFound());
+    }
+
+    @Test
+    public void testSaveAssetWithEmptyType() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        doPost("/api/asset", asset)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Asset type should be specified")));
+    }
+
+    @Test
+    public void testSaveAssetWithEmptyName() throws Exception {
+        Asset asset = new Asset();
+        asset.setType("default");
+        doPost("/api/asset", asset)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Asset name should be specified")));
+    }
+
+    @Test
+    public void testAssignUnassignAssetToCustomer() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        Asset assignedAsset = doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+                + "/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(savedCustomer.getId(), assignedAsset.getCustomerId());
+
+        Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(savedCustomer.getId(), foundAsset.getCustomerId());
+
+        Asset unassignedAsset =
+                doDelete("/api/customer/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, unassignedAsset.getCustomerId().getId());
+
+        foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId());
+    }
+
+    @Test
+    public void testAssignAssetToNonExistentCustomer() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        doPost("/api/customer/" + UUIDs.timeBased().toString()
+                + "/asset/" + savedAsset.getId().getId().toString())
+                .andExpect(status().isNotFound());
+    }
+
+    @Test
+    public void testAssignAssetToCustomerFromDifferentTenant() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant2 = new Tenant();
+        tenant2.setTitle("Different tenant");
+        Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class);
+        Assert.assertNotNull(savedTenant2);
+
+        User tenantAdmin2 = new User();
+        tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin2.setTenantId(savedTenant2.getId());
+        tenantAdmin2.setEmail("tenant3@thingsboard.org");
+        tenantAdmin2.setFirstName("Joe");
+        tenantAdmin2.setLastName("Downs");
+
+        tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1");
+
+        Customer customer = new Customer();
+        customer.setTitle("Different customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        login(tenantAdmin.getEmail(), "testPassword1");
+
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+                + "/asset/" + savedAsset.getId().getId().toString())
+                .andExpect(status().isForbidden());
+
+        loginSysAdmin();
+
+        doDelete("/api/tenant/"+savedTenant2.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testFindTenantAssets() throws Exception {
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<178;i++) {
+            Asset asset = new Asset();
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+    }
+
+    @Test
+    public void testFindTenantAssetsByName() throws Exception {
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle1.add(doPost("/api/asset", asset, Asset.class));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<75;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle2.add(doPost("/api/asset", asset, Asset.class));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindTenantAssetsByType() throws Exception {
+        String title1 = "Asset title 1";
+        String type1 = "typeA";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            assetsType1.add(doPost("/api/asset", asset, Asset.class));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeB";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<75;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            assetsType2.add(doPost("/api/asset", asset, Asset.class));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindCustomerAssets() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<128;i++) {
+            Asset asset = new Asset();
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            asset = doPost("/api/asset", asset, Asset.class);
+            assets.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+    }
+
+    @Test
+    public void testFindCustomerAssetsByName() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsTitle1.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsTitle2.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindCustomerAssetsByType() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        String type1 = "typeC";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsType1.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeD";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsType2.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
index 304385d..26eaa5d 100644
--- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
@@ -25,10 +25,7 @@ import java.util.List;
 
 import com.datastax.driver.core.utils.UUIDs;
 import org.apache.commons.lang3.RandomStringUtils;
-import org.thingsboard.server.common.data.Customer;
-import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.*;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceCredentialsId;
 import org.thingsboard.server.common.data.id.DeviceId;
@@ -83,6 +80,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDevice() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         Assert.assertNotNull(savedDevice);
@@ -114,16 +112,49 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testFindDeviceById() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
         Assert.assertNotNull(foundDevice);
         Assert.assertEquals(savedDevice, foundDevice);
     }
+
+    @Test
+    public void testFindDeviceTypesByTenantId() throws Exception {
+        List<Device> devices = new ArrayList<>();
+        for (int i=0;i<3;i++) {
+            Device device = new Device();
+            device.setName("My device B"+i);
+            device.setType("typeB");
+            devices.add(doPost("/api/device", device, Device.class));
+        }
+        for (int i=0;i<7;i++) {
+            Device device = new Device();
+            device.setName("My device C"+i);
+            device.setType("typeC");
+            devices.add(doPost("/api/device", device, Device.class));
+        }
+        for (int i=0;i<9;i++) {
+            Device device = new Device();
+            device.setName("My device A"+i);
+            device.setType("typeA");
+            devices.add(doPost("/api/device", device, Device.class));
+        }
+        List<TenantDeviceType> deviceTypes = doGetTyped("/api/device/types",
+                new TypeReference<List<TenantDeviceType>>(){});
+
+        Assert.assertNotNull(deviceTypes);
+        Assert.assertEquals(3, deviceTypes.size());
+        Assert.assertEquals("typeA", deviceTypes.get(0).getType());
+        Assert.assertEquals("typeB", deviceTypes.get(1).getType());
+        Assert.assertEquals("typeC", deviceTypes.get(2).getType());
+    }
     
     @Test
     public void testDeleteDevice() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         doDelete("/api/device/"+savedDevice.getId().getId().toString())
@@ -132,10 +163,20 @@ public class DeviceControllerTest extends AbstractControllerTest {
         doGet("/api/device/"+savedDevice.getId().getId().toString())
         .andExpect(status().isNotFound());
     }
-    
+
+    @Test
+    public void testSaveDeviceWithEmptyType() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        doPost("/api/device", device)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Device type should be specified")));
+    }
+
     @Test
     public void testSaveDeviceWithEmptyName() throws Exception {
         Device device = new Device();
+        device.setType("default");
         doPost("/api/device", device)
         .andExpect(status().isBadRequest())
         .andExpect(statusReason(containsString("Device name should be specified")));
@@ -145,6 +186,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testAssignUnassignDeviceToCustomer() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         Customer customer = new Customer();
@@ -170,6 +212,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testAssignDeviceToNonExistentCustomer() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         doPost("/api/customer/" + UUIDs.timeBased().toString()
@@ -203,6 +246,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
         
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         doPost("/api/customer/" + savedCustomer.getId().getId().toString()
@@ -219,6 +263,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testFindDeviceCredentialsByDeviceId() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); 
@@ -229,6 +274,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentials() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); 
@@ -255,6 +301,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsType() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -268,6 +315,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsId() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -281,6 +329,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveNonExistentDeviceCredentials() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -298,6 +347,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentialsWithNonExistentDevice() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -307,9 +357,10 @@ public class DeviceControllerTest extends AbstractControllerTest {
     }
     
     @Test
-    public void testSaveDeviceCredentialsWithInvalidCredemtialsIdLength() throws Exception {
+    public void testSaveDeviceCredentialsWithInvalidCredentialsIdLength() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -325,6 +376,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
         for (int i=0;i<178;i++) {
             Device device = new Device();
             device.setName("Device"+i);
+            device.setType("default");
             devices.add(doPost("/api/device", device, Device.class));
         }
         List<Device> loadedDevices = new ArrayList<>();
@@ -355,6 +407,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle1.add(doPost("/api/device", device, Device.class));
         }
         String title2 = "Device title 2";
@@ -365,6 +418,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle2.add(doPost("/api/device", device, Device.class));
         }
         
@@ -423,6 +477,89 @@ public class DeviceControllerTest extends AbstractControllerTest {
         Assert.assertFalse(pageData.hasNext());
         Assert.assertEquals(0, pageData.getData().size());
     }
+
+    @Test
+    public void testFindTenantDevicesByType() throws Exception {
+        String title1 = "Device title 1";
+        String type1 = "typeA";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            devicesType1.add(doPost("/api/device", device, Device.class));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeB";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<75;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            devicesType2.add(doPost("/api/device", device, Device.class));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            doDelete("/api/device/"+device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            doDelete("/api/device/"+device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
     
     @Test
     public void testFindCustomerDevices() throws Exception {
@@ -435,6 +572,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
         for (int i=0;i<128;i++) {
             Device device = new Device();
             device.setName("Device"+i);
+            device.setType("default");
             device = doPost("/api/device", device, Device.class);
             devices.add(doPost("/api/customer/" + customerId.getId().toString() 
                             + "/device/" + device.getId().getId().toString(), Device.class));
@@ -473,6 +611,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = doPost("/api/device", device, Device.class);
             devicesTitle1.add(doPost("/api/customer/" + customerId.getId().toString() 
                     + "/device/" + device.getId().getId().toString(), Device.class));
@@ -485,6 +624,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = doPost("/api/device", device, Device.class);
             devicesTitle2.add(doPost("/api/customer/" + customerId.getId().toString() 
                     + "/device/" + device.getId().getId().toString(), Device.class));
@@ -546,4 +686,96 @@ public class DeviceControllerTest extends AbstractControllerTest {
         Assert.assertEquals(0, pageData.getData().size());
     }
 
+    @Test
+    public void testFindCustomerDevicesByType() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Device title 1";
+        String type1 = "typeC";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            device = doPost("/api/device", device, Device.class);
+            devicesType1.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/device/" + device.getId().getId().toString(), Device.class));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeD";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            device = doPost("/api/device", device, Device.class);
+            devicesType2.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/device/" + device.getId().getId().toString(), Device.class));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            doDelete("/api/customer/device/" + device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            doDelete("/api/customer/device/" + device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
 }
diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
index 5d72076..dc4a422 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
@@ -130,7 +130,7 @@ public class TenantControllerTest extends AbstractControllerTest {
         Assert.assertEquals(tenants, loadedTenants);
         
         for (Tenant tenant : loadedTenants) {
-            if (!tenant.getTitle().equals("Tenant")) {
+            if (!tenant.getTitle().equals(TEST_TENANT_NAME)) {
                 doDelete("/api/tenant/"+tenant.getId().getId().toString())
                 .andExpect(status().isOk());        
             }
diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
index 7c00049..4fafd61 100644
--- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
@@ -182,7 +182,7 @@ public class UserControllerTest extends AbstractControllerTest {
         Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
         Assert.assertNotNull(savedTenant);
         
-        String email = "tenant@thingsboard.org";
+        String email = TENANT_ADMIN_EMAIL;
         User user = new User();
         user.setAuthority(Authority.TENANT_ADMIN);
         user.setTenantId(savedTenant.getId());
diff --git a/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
index b65a5a6..af8b50f 100644
--- a/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
@@ -47,6 +47,7 @@ public class HttpDeviceApiTest extends AbstractControllerTest {
         loginTenantAdmin();
         device = new Device();
         device.setName("My device");
+        device.setType("default");
         device = doPost("/api/device", device, Device.class);
 
         deviceCredentials =
diff --git a/common/data/pom.xml b/common/data/pom.xml
index aca4c9c..ffee17c 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 6691670..09695e5 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
@@ -15,12 +15,13 @@
  */
 package org.thingsboard.server.common.data.alarm;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.AllArgsConstructor;
 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,9 @@ public class Alarm extends BaseData<AlarmId> {
         super(id);
     }
 
+    @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    public String getName() {
+        return type;
+    }
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
index 2dc0189..00ca6c3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
@@ -15,6 +15,8 @@
  */
 package org.thingsboard.server.common.data.alarm;
 
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Data;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -24,9 +26,10 @@ import org.thingsboard.server.common.data.page.TimePageLink;
  * Created by ashvayka on 11.05.17.
  */
 @Data
+@Builder
+@AllArgsConstructor
 public class AlarmQuery {
 
-    private TenantId tenantId;
     private EntityId affectedEntityId;
     private TimePageLink pageLink;
     private AlarmStatus status;
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/asset/TenantAssetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/TenantAssetType.java
new file mode 100644
index 0000000..8e0eb0e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/TenantAssetType.java
@@ -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.
+ */
+package org.thingsboard.server.common.data.asset;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.UUID;
+
+public class TenantAssetType {
+
+    private static final long serialVersionUID = 8057290243855622101L;
+
+    private String type;
+    private TenantId tenantId;
+
+    public TenantAssetType() {
+        super();
+    }
+
+    public TenantAssetType(String type, TenantId tenantId) {
+        this.type = type;
+        this.tenantId = tenantId;
+    }
+
+    public TenantAssetType(String type, UUID tenantId) {
+        this.type = type;
+        this.tenantId = new TenantId(tenantId);
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantAssetType that = (TenantAssetType) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantAssetType{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+}
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..ec535bf 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
@@ -15,12 +15,14 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonProperty.Access;
 import org.thingsboard.server.common.data.id.CustomerId;
 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 +61,12 @@ public class Customer extends ContactBased<CustomerId>{
         this.title = title;
     }
 
+    @Override
+    @JsonProperty(access = Access.READ_ONLY)
+    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..3be3b79 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
@@ -15,11 +15,12 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 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 +66,12 @@ public class DashboardInfo extends SearchTextBased<DashboardId> {
     }
 
     @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    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 6b31283..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,13 +21,14 @@ 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;
 
     private TenantId tenantId;
     private CustomerId customerId;
     private String name;
+    private String type;
     private JsonNode additionalInfo;
 
     public Device() {
@@ -43,6 +44,7 @@ public class Device extends SearchTextBased<DeviceId> {
         this.tenantId = device.getTenantId();
         this.customerId = device.getCustomerId();
         this.name = device.getName();
+        this.type = device.getType();
         this.additionalInfo = device.getAdditionalInfo();
     }
 
@@ -62,6 +64,7 @@ public class Device extends SearchTextBased<DeviceId> {
         this.customerId = customerId;
     }
 
+    @Override
     public String getName() {
         return name;
     }
@@ -70,6 +73,14 @@ public class Device extends SearchTextBased<DeviceId> {
         this.name = name;
     }
 
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
     public JsonNode getAdditionalInfo() {
         return additionalInfo;
     }
@@ -90,6 +101,7 @@ public class Device extends SearchTextBased<DeviceId> {
         result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
         result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
         result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((type == null) ? 0 : type.hashCode());
         result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
         return result;
     }
@@ -118,6 +130,11 @@ public class Device extends SearchTextBased<DeviceId> {
                 return false;
         } else if (!name.equals(other.name))
             return false;
+        if (type == null) {
+            if (other.type != null)
+                return false;
+        } else if (!type.equals(other.type))
+            return false;
         if (tenantId == null) {
             if (other.tenantId != null)
                 return false;
@@ -135,6 +152,8 @@ public class Device extends SearchTextBased<DeviceId> {
         builder.append(customerId);
         builder.append(", name=");
         builder.append(name);
+        builder.append(", type=");
+        builder.append(type);
         builder.append(", additionalInfo=");
         builder.append(additionalInfo);
         builder.append(", createdTime=");
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/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index 21093ce..1f77904 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.common.data.id;
 
 import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.alarm.AlarmId;
 
 import java.util.UUID;
 
@@ -50,6 +51,8 @@ public class EntityIdFactory {
                 return new DeviceId(uuid);
             case ASSET:
                 return new AssetId(uuid);
+            case ALARM:
+                return new AlarmId(uuid);
         }
         throw new IllegalArgumentException("EntityType " + type + " is not supported!");
     }
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..8dab589 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
@@ -30,6 +30,7 @@ public class EntityRelation {
     private EntityId from;
     private EntityId to;
     private String type;
+    private RelationTypeGroup typeGroup;
     private JsonNode additionalInfo;
 
     public EntityRelation() {
@@ -37,21 +38,27 @@ public class EntityRelation {
     }
 
     public EntityRelation(EntityId from, EntityId to, String type) {
-        this(from, to, type, null);
+        this(from, to, type, RelationTypeGroup.COMMON);
     }
 
-    public EntityRelation(EntityId from, EntityId to, String type, JsonNode additionalInfo) {
+    public EntityRelation(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) {
+        this(from, to, type, typeGroup, null);
+    }
+
+    public EntityRelation(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup, JsonNode additionalInfo) {
         this.from = from;
         this.to = to;
         this.type = type;
+        this.typeGroup = typeGroup;
         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.typeGroup = entityRelation.getTypeGroup();
+        this.additionalInfo = entityRelation.getAdditionalInfo();
     }
 
     public EntityId getFrom() {
@@ -78,6 +85,14 @@ public class EntityRelation {
         this.type = type;
     }
 
+    public RelationTypeGroup getTypeGroup() {
+        return typeGroup;
+    }
+
+    public void setTypeGroup(RelationTypeGroup typeGroup) {
+        this.typeGroup = typeGroup;
+    }
+
     public JsonNode getAdditionalInfo() {
         return additionalInfo;
     }
@@ -90,14 +105,22 @@ public class EntityRelation {
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
-        EntityRelation relation = (EntityRelation) o;
-        return Objects.equals(from, relation.from) &&
-                Objects.equals(to, relation.to) &&
-                Objects.equals(type, relation.type);
+
+        EntityRelation that = (EntityRelation) o;
+
+        if (from != null ? !from.equals(that.from) : that.from != null) return false;
+        if (to != null ? !to.equals(that.to) : that.to != null) return false;
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return typeGroup == that.typeGroup;
+
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(from, to, type);
+        int result = from != null ? from.hashCode() : 0;
+        result = 31 * result + (to != null ? to.hashCode() : 0);
+        result = 31 * result + (type != null ? type.hashCode() : 0);
+        result = 31 * result + (typeGroup != null ? typeGroup.hashCode() : 0);
+        return result;
     }
 }
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/relation/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
new file mode 100644
index 0000000..82798ab
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
@@ -0,0 +1,23 @@
+/**
+ * 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 enum RelationTypeGroup {
+
+    COMMON,
+    ALARM
+
+}
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 ececfdd..1451d2c 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,13 +17,14 @@ package org.thingsboard.server.common.data.rule;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.Data;
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.SearchTextBased;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 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;
 
@@ -63,4 +64,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..4501759 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
@@ -15,11 +15,12 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 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 +51,12 @@ public class Tenant extends ContactBased<TenantId>{
         this.title = title;
     }
 
+    @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    public String getName() {
+        return title;
+    }
+
     public String getRegion() {
         return region;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantDeviceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantDeviceType.java
new file mode 100644
index 0000000..d611a25
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantDeviceType.java
@@ -0,0 +1,79 @@
+/**
+ * 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;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+public class TenantDeviceType {
+
+    private static final long serialVersionUID = 8057240243859922101L;
+
+    private String type;
+    private TenantId tenantId;
+
+    public TenantDeviceType() {
+        super();
+    }
+
+    public TenantDeviceType(String type, TenantId tenantId) {
+        this.type = type;
+        this.tenantId = tenantId;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantDeviceType that = (TenantDeviceType) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantDeviceType{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+}
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..1b97e2e 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
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
@@ -22,7 +23,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 +78,12 @@ public class User extends SearchTextBased<UserId> {
         this.email = email;
     }
 
+    @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    public String getName() {
+        return email;
+    }
+
     public Authority getAuthority() {
         return authority;
     }
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 01d3b45..5362f68 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>

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

diff --git a/common/pom.xml b/common/pom.xml
index 9c7b51e..798bec4 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 4a2bb52..c537756 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>

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

diff --git a/dao/pom.xml b/dao/pom.xml
index ced5c38..a3c9b2c 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
index a178c6e..712bbd1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
@@ -17,10 +17,12 @@ package org.thingsboard.server.dao.alarm;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.dao.Dao;
 
+import java.util.List;
 import java.util.UUID;
 
 /**
@@ -30,4 +32,9 @@ public interface AlarmDao extends Dao<Alarm> {
 
     ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
 
+    ListenableFuture<Alarm> findAlarmByIdAsync(UUID key);
+
+    Alarm save(Alarm alarm);
+
+    ListenableFuture<List<Alarm>> findAlarms(AlarmQuery query);
 }
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 6508db2..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
@@ -16,16 +16,11 @@
 package org.thingsboard.server.dao.alarm;
 
 import com.google.common.util.concurrent.ListenableFuture;
-import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.alarm.Alarm;
 import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.alarm.AlarmQuery;
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.page.TimePageData;
 
-import java.util.Optional;
-
 /**
  * Created by ashvayka on 11.05.17.
  */
@@ -33,13 +28,11 @@ public interface AlarmService {
 
     Alarm createOrUpdateAlarm(Alarm alarm);
 
-    ListenableFuture<Boolean> updateAlarm(Alarm alarm);
-
     ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTs);
 
     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 59ec547..77cc574 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
@@ -31,7 +31,8 @@ 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.BaseEntityService;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.relation.EntityRelationsQuery;
 import org.thingsboard.server.dao.relation.EntitySearchDirection;
@@ -41,18 +42,22 @@ import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.tenant.TenantDao;
 
 import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.dao.service.Validator.validateId;
 
 @Service
 @Slf4j
-public class BaseAlarmService extends BaseEntityService implements AlarmService {
+public class BaseAlarmService extends AbstractEntityService implements AlarmService {
 
-    private static final String ALARM_RELATION_PREFIX = "ALARM_";
-    private static final String ALARM_RELATION = "ALARM_ANY";
+    public static final String ALARM_RELATION_PREFIX = "ALARM_";
+    public static final String ALARM_RELATION = "ALARM_ANY";
 
     @Autowired
     private AlarmDao alarmDao;
@@ -63,6 +68,20 @@ public class BaseAlarmService extends BaseEntityService implements AlarmService 
     @Autowired
     private RelationService relationService;
 
+    protected ExecutorService readResultsProcessingExecutor;
+
+    @PostConstruct
+    public void startExecutor() {
+        readResultsProcessingExecutor = Executors.newCachedThreadPool();
+    }
+
+    @PreDestroy
+    public void stopExecutor() {
+        if (readResultsProcessingExecutor != null) {
+            readResultsProcessingExecutor.shutdownNow();
+        }
+    }
+
     @Override
     public Alarm createOrUpdateAlarm(Alarm alarm) {
         alarmDataValidator.validate(alarm);
@@ -73,51 +92,61 @@ public class BaseAlarmService extends BaseEntityService implements AlarmService 
             if (alarm.getEndTs() == 0L) {
                 alarm.setEndTs(alarm.getStartTs());
             }
-            Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get();
-            if (existing == null || existing.getStatus().isCleared()) {
-                log.debug("New Alarm : {}", alarm);
-                Alarm saved = alarmDao.save(alarm);
-                EntityRelationsQuery query = new EntityRelationsQuery();
-                query.setParameters(new RelationsSearchParameters(saved.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
-                List<EntityId> parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList());
-                for (EntityId parentId : parentEntities) {
-                    createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION));
-                    createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name()));
+            if (alarm.getId() == null) {
+                Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get();
+                if (existing == null || existing.getStatus().isCleared()) {
+                    return createAlarm(alarm);
+                } else {
+                    return updateAlarm(existing, alarm);
                 }
-                return saved;
             } else {
-                log.debug("Alarm before merge: {}", alarm);
-                alarm = merge(existing, alarm);
-                log.debug("Alarm after merge: {}", alarm);
-                return alarmDao.save(alarm);
+                return updateAlarm(alarm).get();
             }
         } catch (ExecutionException | InterruptedException e) {
             throw new RuntimeException(e);
         }
     }
 
-    @Override
-    public ListenableFuture<Boolean> updateAlarm(Alarm update) {
+    private Alarm createAlarm(Alarm alarm) throws InterruptedException, ExecutionException {
+        log.debug("New Alarm : {}", alarm);
+        Alarm saved = alarmDao.save(alarm);
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(new RelationsSearchParameters(saved.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
+        List<EntityId> parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList());
+        for (EntityId parentId : parentEntities) {
+            createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION, RelationTypeGroup.ALARM));
+            createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name(), RelationTypeGroup.ALARM));
+        }
+        createRelation(new EntityRelation(alarm.getOriginator(), saved.getId(), ALARM_RELATION, RelationTypeGroup.ALARM));
+        createRelation(new EntityRelation(alarm.getOriginator(), saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name(), RelationTypeGroup.ALARM));
+        return saved;
+    }
+
+    protected ListenableFuture<Alarm> updateAlarm(Alarm update) {
         alarmDataValidator.validate(update);
-        return getAndUpdate(update.getId(), new Function<Alarm, Boolean>() {
+        return getAndUpdate(update.getId(), new Function<Alarm, Alarm>() {
             @Nullable
             @Override
-            public Boolean apply(@Nullable Alarm alarm) {
+            public Alarm apply(@Nullable Alarm alarm) {
                 if (alarm == null) {
-                    return false;
+                    return null;
                 } else {
-                    AlarmStatus oldStatus = alarm.getStatus();
-                    AlarmStatus newStatus = update.getStatus();
-                    alarmDao.save(merge(alarm, update));
-                    if (oldStatus != newStatus) {
-                        updateRelations(alarm, oldStatus, newStatus);
-                    }
-                    return true;
+                    return updateAlarm(alarm, update);
                 }
             }
         });
     }
 
+    private Alarm updateAlarm(Alarm oldAlarm, Alarm newAlarm) {
+        AlarmStatus oldStatus = oldAlarm.getStatus();
+        AlarmStatus newStatus = newAlarm.getStatus();
+        Alarm result = alarmDao.save(merge(oldAlarm, newAlarm));
+        if (oldStatus != newStatus) {
+            updateRelations(oldAlarm, oldStatus, newStatus);
+        }
+        return result;
+    }
+
     @Override
     public ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTime) {
         return getAndUpdate(alarmId, new Function<Alarm, Boolean>() {
@@ -161,7 +190,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.findByIdAsync(alarmId.getId());
@@ -169,7 +198,14 @@ public class BaseAlarmService extends BaseEntityService implements AlarmService 
 
     @Override
     public ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query) {
-        return null;
+        ListenableFuture<List<Alarm>> alarms = alarmDao.findAlarms(query);
+        return Futures.transform(alarms, new Function<List<Alarm>, TimePageData<Alarm>>() {
+            @Nullable
+            @Override
+            public TimePageData<Alarm> apply(@Nullable List<Alarm> alarms) {
+                return new TimePageData<>(alarms, query.getPageLink());
+            }
+        });
     }
 
     private void deleteRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
@@ -207,19 +243,21 @@ public class BaseAlarmService extends BaseEntityService implements AlarmService 
             query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
             List<EntityId> parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList());
             for (EntityId parentId : parentEntities) {
-                deleteRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name()));
-                createRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name()));
+                deleteRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name(), RelationTypeGroup.ALARM));
+                createRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name(), RelationTypeGroup.ALARM));
             }
+            deleteRelation(new EntityRelation(alarm.getOriginator(), alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name(), RelationTypeGroup.ALARM));
+            createRelation(new EntityRelation(alarm.getOriginator(), alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name(), RelationTypeGroup.ALARM));
         } catch (ExecutionException | InterruptedException e) {
             log.warn("[{}] Failed to update relations. Old status: [{}], New status: [{}]", alarm.getId(), oldStatus, newStatus);
             throw new RuntimeException(e);
         }
     }
 
-    private ListenableFuture<Boolean> getAndUpdate(AlarmId alarmId, Function<Alarm, Boolean> function) {
+    private <T> ListenableFuture<T> getAndUpdate(AlarmId alarmId, Function<Alarm, T> function) {
         validateId(alarmId, "Alarm id should be specified!");
-        ListenableFuture<Alarm> entity = alarmDao.findByIdAsync(alarmId.getId());
-        return Futures.transform(entity, function);
+        ListenableFuture<Alarm> entity = alarmDao.findAlarmByIdAsync(alarmId.getId());
+        return Futures.transform(entity, function, readResultsProcessingExecutor);
     }
 
     private DataValidator<Alarm> alarmDataValidator =
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
index 378f0ee..926127e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
@@ -17,16 +17,27 @@ package org.thingsboard.server.dao.alarm;
 
 import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
+import com.google.common.util.concurrent.AsyncFunction;
+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.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.CassandraAbstractModelDao;
-import org.thingsboard.server.dao.model.nosql.AlarmEntity;
 import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.nosql.AlarmEntity;
+import org.thingsboard.server.dao.relation.RelationDao;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.UUID;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
@@ -35,8 +46,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraAlarmDao extends CassandraAbstractModelDao<AlarmEntity, Alarm> implements AlarmDao {
 
+    @Autowired
+    private RelationDao relationDao;
+
     @Override
     protected Class<AlarmEntity> getColumnFamilyClass() {
         return AlarmEntity.class;
@@ -47,6 +62,10 @@ public class CassandraAlarmDao extends CassandraAbstractModelDao<AlarmEntity, Al
         return ALARM_COLUMN_FAMILY_NAME;
     }
 
+    protected boolean isDeleteOnSave() {
+        return false;
+    }
+
     @Override
     public Alarm save(Alarm alarm) {
         log.debug("Save asset [{}] ", alarm);
@@ -65,4 +84,24 @@ public class CassandraAlarmDao extends CassandraAbstractModelDao<AlarmEntity, Al
         query.orderBy(QueryBuilder.asc(ModelConstants.ALARM_TYPE_PROPERTY), QueryBuilder.desc(ModelConstants.ID_PROPERTY));
         return findOneByStatementAsync(query);
     }
+
+    @Override
+    public ListenableFuture<List<Alarm>> findAlarms(AlarmQuery query) {
+        log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink());
+        EntityId affectedEntity = query.getAffectedEntityId();
+        String relationType = query.getStatus() == null ? BaseAlarmService.ALARM_RELATION : BaseAlarmService.ALARM_RELATION_PREFIX + query.getStatus().name();
+        ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
+        return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Alarm>>) input -> {
+            List<ListenableFuture<Alarm>> alarmFutures = new ArrayList<>(input.size());
+            for (EntityRelation relation : input) {
+                alarmFutures.add(findAlarmByIdAsync(relation.getTo().getId()));
+            }
+            return Futures.successfulAsList(alarmFutures);
+        });
+    }
+
+    @Override
+    public ListenableFuture<Alarm> findAlarmByIdAsync(UUID key) {
+        return findByIdAsync(key);
+    }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
index b002a98..2b720ae 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
@@ -17,8 +17,10 @@ package org.thingsboard.server.dao.asset;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.TenantAssetTypeEntity;
 
 import java.util.List;
 import java.util.Optional;
@@ -48,6 +50,16 @@ public interface AssetDao extends Dao<Asset> {
     List<Asset> findAssetsByTenantId(UUID tenantId, TextPageLink pageLink);
 
     /**
+     * Find assets by tenantId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of asset objects
+     */
+    List<Asset> findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink);
+
+    /**
      * Find assets by tenantId and assets Ids.
      *
      * @param tenantId the tenantId
@@ -67,6 +79,17 @@ public interface AssetDao extends Dao<Asset> {
     List<Asset> findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
 
     /**
+     * Find assets by tenantId, customerId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param customerId the customerId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of asset objects
+     */
+    List<Asset> findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink);
+
+    /**
      * Find assets by tenantId, customerId and assets Ids.
      *
      * @param tenantId the tenantId
@@ -84,4 +107,12 @@ public interface AssetDao extends Dao<Asset> {
      * @return the optional asset object
      */
     Optional<Asset> findAssetsByTenantIdAndName(UUID tenantId, String name);
+
+    /**
+     * Find tenants asset types.
+     *
+     * @return the list of tenant asset type objects
+     */
+    ListenableFuture<List<TenantAssetType>> findTenantAssetTypesAsync();
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
index 7a61bd8..25baeda 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.asset;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -34,7 +35,7 @@ public interface AssetService {
 
     Optional<Asset> findAssetByTenantIdAndName(TenantId tenantId, String name);
 
-    Asset saveAsset(Asset device);
+    Asset saveAsset(Asset asset);
 
     Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId);
 
@@ -44,16 +45,21 @@ public interface AssetService {
 
     TextPageData<Asset> findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink);
 
+    TextPageData<Asset> findAssetsByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds);
 
     void deleteAssetsByTenantId(TenantId tenantId);
 
     TextPageData<Asset> findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
 
+    TextPageData<Asset> findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<AssetId> assetIds);
 
     void unassignCustomerAssets(TenantId tenantId, CustomerId customerId);
 
     ListenableFuture<List<Asset>> findAssetsByQuery(AssetSearchQuery query);
 
+    ListenableFuture<List<TenantAssetType>> findAssetTypesByTenantId(TenantId tenantId);
 }
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 c664e62..a3cd65e 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
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.EntityId;
@@ -36,8 +37,9 @@ 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;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
@@ -45,6 +47,7 @@ import org.thingsboard.server.dao.tenant.TenantDao;
 
 import javax.annotation.Nullable;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -55,7 +58,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;
@@ -126,6 +129,16 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
     }
 
     @Override
+    public TextPageData<Asset> findAssetsByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink) {
+        log.trace("Executing findAssetsByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<Asset> assets = assetDao.findAssetsByTenantIdAndType(tenantId.getId(), type, pageLink);
+        return new TextPageData<>(assets, pageLink);
+    }
+
+    @Override
     public ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds) {
         log.trace("Executing findAssetsByTenantIdAndIdsAsync, tenantId [{}], assetIds [{}]", tenantId, assetIds);
         validateId(tenantId, "Incorrect tenantId " + tenantId);
@@ -151,6 +164,17 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
     }
 
     @Override
+    public TextPageData<Asset> findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink) {
+        log.trace("Executing findAssetsByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<Asset> assets = assetDao.findAssetsByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
+        return new TextPageData<>(assets, pageLink);
+    }
+
+    @Override
     public ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<AssetId> assetIds) {
         log.trace("Executing findAssetsByTenantIdAndCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], assetIds [{}]", tenantId, customerId, assetIds);
         validateId(tenantId, "Incorrect tenantId " + tenantId);
@@ -193,6 +217,25 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
         return assets;
     }
 
+    @Override
+    public ListenableFuture<List<TenantAssetType>> findAssetTypesByTenantId(TenantId tenantId) {
+        log.trace("Executing findAssetTypesByTenantId, tenantId [{}]", tenantId);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        ListenableFuture<List<TenantAssetType>> tenantAssetTypeEntities = assetDao.findTenantAssetTypesAsync();
+        ListenableFuture<List<TenantAssetType>> tenantAssetTypes = Futures.transform(tenantAssetTypeEntities,
+                (Function<List<TenantAssetType>, List<TenantAssetType>>) assetTypeEntities -> {
+                    List<TenantAssetType> assetTypes = new ArrayList<>();
+                    for (TenantAssetType assetType : assetTypeEntities) {
+                        if (assetType.getTenantId().equals(tenantId.getId())) {
+                            assetTypes.add(assetType);
+                        }
+                    }
+                    assetTypes.sort(Comparator.comparing(TenantAssetType::getType));
+                    return assetTypes;
+                });
+        return tenantAssetTypes;
+    }
+
     private DataValidator<Asset> assetValidator =
             new DataValidator<Asset>() {
 
@@ -218,6 +261,9 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
 
                 @Override
                 protected void validateDataImpl(Asset asset) {
+                    if (StringUtils.isEmpty(asset.getType())) {
+                        throw new DataValidationException("Asset type should be specified!");
+                    }
                     if (StringUtils.isEmpty(asset.getName())) {
                         throw new DataValidationException("Asset name should be specified!");
                     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
index 6dca69f..d53a2e4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
@@ -15,16 +15,25 @@
  */
 package org.thingsboard.server.dao.asset;
 
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.mapping.Result;
+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.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.CassandraAbstractSearchTextDao;
 import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.TenantAssetTypeEntity;
 import org.thingsboard.server.dao.model.nosql.AssetEntity;
 
+import javax.annotation.Nullable;
 import java.util.*;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
@@ -32,6 +41,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntity, Asset> implements AssetDao {
 
     @Override
@@ -61,6 +71,15 @@ public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntit
     }
 
     @Override
+    public List<Asset> findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
+        log.debug("Try to find assets by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink);
+        List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(ASSET_TYPE_PROPERTY, type),
+                        eq(ASSET_TENANT_ID_PROPERTY, tenantId)), pageLink);
+        log.trace("Found assets [{}] by tenantId [{}], type [{}] and pageLink [{}]", assetEntities, tenantId, type, pageLink);
+        return DaoUtil.convertDataList(assetEntities);
+    }
+
     public ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List<UUID> assetIds) {
         log.debug("Try to find assets by tenantId [{}] and asset Ids [{}]", tenantId, assetIds);
         Select select = select().from(getColumnFamilyName());
@@ -83,6 +102,19 @@ public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntit
     }
 
     @Override
+    public List<Asset> findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
+        log.debug("Try to find assets by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", tenantId, customerId, type, pageLink);
+        List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(ASSET_TYPE_PROPERTY, type),
+                        eq(ASSET_CUSTOMER_ID_PROPERTY, customerId),
+                        eq(ASSET_TENANT_ID_PROPERTY, tenantId)),
+                pageLink);
+
+        log.trace("Found assets [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", assetEntities, tenantId, customerId, type, pageLink);
+        return DaoUtil.convertDataList(assetEntities);
+    }
+
+    @Override
     public ListenableFuture<List<Asset>> findAssetsByTenantIdAndCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> assetIds) {
         log.debug("Try to find assets by tenantId [{}], customerId [{}] and asset Ids [{}]", tenantId, customerId, assetIds);
         Select select = select().from(getColumnFamilyName());
@@ -103,4 +135,37 @@ public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntit
         return Optional.ofNullable(DaoUtil.getData(assetEntity));
     }
 
+    @Override
+    public ListenableFuture<List<TenantAssetType>> findTenantAssetTypesAsync() {
+        Select statement = select().distinct().column(ASSET_TYPE_PROPERTY).column(ASSET_TENANT_ID_PROPERTY).from(ASSET_TYPES_BY_TENANT_VIEW_NAME);
+        statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
+        ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+        ListenableFuture<List<TenantAssetTypeEntity>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<TenantAssetTypeEntity>>() {
+            @Nullable
+            @Override
+            public List<TenantAssetTypeEntity> apply(@Nullable ResultSet resultSet) {
+                Result<TenantAssetTypeEntity> result = cluster.getMapper(TenantAssetTypeEntity.class).map(resultSet);
+                if (result != null) {
+                    return result.all();
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        });
+        return Futures.transform(result, new Function<List<TenantAssetTypeEntity>, List<TenantAssetType>>() {
+            @Nullable
+            @Override
+            public List<TenantAssetType> apply(@Nullable List<TenantAssetTypeEntity> entityList) {
+                List<TenantAssetType> list = Collections.emptyList();
+                if (entityList != null && !entityList.isEmpty()) {
+                    list = new ArrayList<>();
+                    for (TenantAssetTypeEntity object : entityList) {
+                        list.add(object.toTenantAssetType());
+                    }
+                }
+                return list;
+            }
+        });
+    }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
index f19dee4..db5babe 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
@@ -22,6 +22,7 @@ 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.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -47,6 +48,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
  */
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraBaseAttributesDao extends CassandraAbstractAsyncDao implements AttributesDao {
 
     private PreparedStatement saveStmt;
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/CassandraAbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java
index b35ed37..0fba69c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java
@@ -131,7 +131,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
         log.debug("Save entity {}", entity);
         if (entity.getId() == null) {
             entity.setId(UUIDs.timeBased());
-        } else {
+        } else if (isDeleteOnSave()) {
             removeById(entity.getId());
         }
         Statement saveStatement = getSaveQuery(entity);
@@ -140,6 +140,10 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
         return new EntityResultSet<>(resultSet, entity);
     }
 
+    protected boolean isDeleteOnSave() {
+        return true;
+    }
+
     @Override
     public D save(D domain) {
         E entity;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractSearchTimeDao.java b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractSearchTimeDao.java
index cb60ea4..bc6b3e7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractSearchTimeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractSearchTimeDao.java
@@ -44,6 +44,26 @@ public abstract class CassandraAbstractSearchTimeDao<E extends BaseEntity<D>, D>
     }
 
     protected List<E> findPageWithTimeSearch(String searchView, List<Clause> clauses, List<Ordering> topLevelOrderings, TimePageLink pageLink) {
+        return findPageWithTimeSearch(searchView, clauses, topLevelOrderings, pageLink, ModelConstants.ID_PROPERTY);
+    }
+
+    protected List<E> findPageWithTimeSearch(String searchView, List<Clause> clauses, TimePageLink pageLink, String idColumn) {
+        return findPageWithTimeSearch(searchView, clauses, Collections.emptyList(), pageLink, idColumn);
+    }
+
+    protected List<E> findPageWithTimeSearch(String searchView, List<Clause> clauses, List<Ordering> topLevelOrderings, TimePageLink pageLink, String idColumn) {
+        return findListByStatement(buildQuery(searchView, clauses, topLevelOrderings, pageLink, idColumn));
+    }
+
+    public static Where buildQuery(String searchView, List<Clause> clauses, TimePageLink pageLink, String idColumn) {
+        return buildQuery(searchView, clauses, Collections.emptyList(), pageLink, idColumn);
+    }
+
+    public static Where buildQuery(String searchView, List<Clause> clauses, Ordering order, TimePageLink pageLink, String idColumn) {
+        return buildQuery(searchView, clauses, Collections.singletonList(order), pageLink, idColumn);
+    }
+
+    public static Where buildQuery(String searchView, List<Clause> clauses, List<Ordering> topLevelOrderings, TimePageLink pageLink, String idColumn) {
         Select select = select().from(searchView);
         Where query = select.where();
         for (Clause clause : clauses) {
@@ -52,34 +72,35 @@ public abstract class CassandraAbstractSearchTimeDao<E extends BaseEntity<D>, D>
         query.limit(pageLink.getLimit());
         if (pageLink.isAscOrder()) {
             if (pageLink.getIdOffset() != null) {
-                query.and(QueryBuilder.gt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset()));
+                query.and(QueryBuilder.gt(idColumn, pageLink.getIdOffset()));
             } else if (pageLink.getStartTime() != null) {
                 final UUID startOf = UUIDs.startOf(pageLink.getStartTime());
-                query.and(QueryBuilder.gte(ModelConstants.ID_PROPERTY, startOf));
+                query.and(QueryBuilder.gte(idColumn, startOf));
             }
             if (pageLink.getEndTime() != null) {
                 final UUID endOf = UUIDs.endOf(pageLink.getEndTime());
-                query.and(QueryBuilder.lte(ModelConstants.ID_PROPERTY, endOf));
+                query.and(QueryBuilder.lte(idColumn, endOf));
             }
         } else {
             if (pageLink.getIdOffset() != null) {
-                query.and(QueryBuilder.lt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset()));
+                query.and(QueryBuilder.lt(idColumn, pageLink.getIdOffset()));
             } else if (pageLink.getEndTime() != null) {
                 final UUID endOf = UUIDs.endOf(pageLink.getEndTime());
-                query.and(QueryBuilder.lte(ModelConstants.ID_PROPERTY, endOf));
+                query.and(QueryBuilder.lte(idColumn, endOf));
             }
             if (pageLink.getStartTime() != null) {
                 final UUID startOf = UUIDs.startOf(pageLink.getStartTime());
-                query.and(QueryBuilder.gte(ModelConstants.ID_PROPERTY, startOf));
+                query.and(QueryBuilder.gte(idColumn, startOf));
             }
         }
         List<Ordering> orderings = new ArrayList<>(topLevelOrderings);
         if (pageLink.isAscOrder()) {
-            orderings.add(QueryBuilder.asc(ModelConstants.ID_PROPERTY));
+            orderings.add(QueryBuilder.asc(idColumn));
         } else {
-            orderings.add(QueryBuilder.desc(ModelConstants.ID_PROPERTY));
+            orderings.add(QueryBuilder.desc(idColumn));
         }
         query.orderBy(orderings.toArray(new Ordering[orderings.size()]));
-        return findListByStatement(query);
+        return query;
     }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
index 92ca9c4..7a1c67d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
@@ -23,6 +23,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID
 import java.util.Optional;
 import com.datastax.driver.core.querybuilder.Select;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -38,6 +39,7 @@ import java.util.UUID;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraCustomerDao extends CassandraAbstractSearchTextDao<CustomerEntity, Customer> implements CustomerDao {
 
     @Override
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 7fa4211..65eb89f 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
@@ -16,9 +16,11 @@
 package org.thingsboard.server.dao.customer;
 
 import static org.thingsboard.server.dao.service.Validator.validateId;
+
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
+
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -34,7 +36,7 @@ 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.service.DataValidator;
@@ -42,27 +44,28 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.service.Validator;
 import org.thingsboard.server.dao.tenant.TenantDao;
 import org.thingsboard.server.dao.user.UserService;
+
 @Service
 @Slf4j
-public class CustomerServiceImpl extends BaseEntityService implements CustomerService {
+public class CustomerServiceImpl extends AbstractEntityService implements CustomerService {
 
     private static final String PUBLIC_CUSTOMER_TITLE = "Public";
 
     @Autowired
     private CustomerDao customerDao;
-    
+
     @Autowired
     private UserService userService;
-    
+
     @Autowired
     private TenantDao tenantDao;
-    
+
     @Autowired
     private DeviceService deviceService;
-    
+
     @Autowired
     private DashboardService dashboardService;
-    
+
     @Override
     public Customer findCustomerById(CustomerId customerId) {
         log.trace("Executing findCustomerById [{}]", customerId);
@@ -134,7 +137,7 @@ public class CustomerServiceImpl extends BaseEntityService implements CustomerSe
         Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
         customersByTenantRemover.removeEntitites(tenantId);
     }
-    
+
     private DataValidator<Customer> customerValidator =
             new DataValidator<Customer>() {
 
@@ -178,19 +181,19 @@ public class CustomerServiceImpl extends BaseEntityService implements CustomerSe
                         }
                     }
                 }
-    };
+            };
 
     private PaginatedRemover<TenantId, Customer> customersByTenantRemover =
             new PaginatedRemover<TenantId, Customer>() {
-        
-        @Override
-        protected List<Customer> findEntities(TenantId id, TextPageLink pageLink) {
-            return customerDao.findCustomersByTenantId(id.getId(), pageLink);
-        }
 
-        @Override
-        protected void removeEntity(Customer entity) {
-            deleteCustomer(new CustomerId(entity.getUuidId()));
-        }
-    };
+                @Override
+                protected List<Customer> findEntities(TenantId id, TextPageLink pageLink) {
+                    return customerDao.findCustomersByTenantId(id.getId(), pageLink);
+                }
+
+                @Override
+                protected void removeEntity(Customer entity) {
+                    deleteCustomer(new CustomerId(entity.getUuidId()));
+                }
+            };
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardDao.java
index 82d5a26..c0ea1dc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardDao.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.dashboard;
 
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Dashboard;
 import org.thingsboard.server.dao.CassandraAbstractSearchTextDao;
@@ -23,6 +24,7 @@ import org.thingsboard.server.dao.model.nosql.DashboardEntity;
 import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
 
 @Component
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraDashboardDao extends CassandraAbstractSearchTextDao<DashboardEntity, Dashboard> implements DashboardDao {
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
index b0c3e89..366ec1f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.dao.dashboard;
 
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.DashboardInfo;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -33,6 +34,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraDashboardInfoDao extends CassandraAbstractSearchTextDao<DashboardInfoEntity, DashboardInfo> implements DashboardInfoDao {
 
     @Override
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 b49b5b4..74d8544 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,6 +28,12 @@ public interface DashboardService {
     
     Dashboard findDashboardById(DashboardId dashboardId);
 
+    ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId);
+
+    DashboardInfo findDashboardInfoById(DashboardId dashboardId);
+
+    ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId);
+
     Dashboard saveDashboard(Dashboard dashboard);
 
     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 bdca536..523f774 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
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.dashboard;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -29,7 +30,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.ModelConstants;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -39,9 +40,11 @@ import org.thingsboard.server.dao.tenant.TenantDao;
 
 import java.util.List;
 
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
 @Service
 @Slf4j
-public class DashboardServiceImpl extends BaseEntityService implements DashboardService {
+public class DashboardServiceImpl extends AbstractEntityService implements DashboardService {
 
     @Autowired
     private DashboardDao dashboardDao;
@@ -63,6 +66,27 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
     }
 
     @Override
+    public ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId) {
+        log.trace("Executing findDashboardByIdAsync [{}]", dashboardId);
+        validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        return dashboardDao.findByIdAsync(dashboardId.getId());
+    }
+
+    @Override
+    public DashboardInfo findDashboardInfoById(DashboardId dashboardId) {
+        log.trace("Executing findDashboardInfoById [{}]", dashboardId);
+        Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        return dashboardInfoDao.findById(dashboardId.getId());
+    }
+
+    @Override
+    public ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId) {
+        log.trace("Executing findDashboardInfoByIdAsync [{}]", dashboardId);
+        validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        return dashboardInfoDao.findByIdAsync(dashboardId.getId());
+    }
+
+    @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/CassandraDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceCredentialsDao.java
index 14ac5bf..517b8cb 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceCredentialsDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceCredentialsDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
 
 import com.datastax.driver.core.querybuilder.Select.Where;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.CassandraAbstractModelDao;
@@ -31,6 +32,7 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraDeviceCredentialsDao extends CassandraAbstractModelDao<DeviceCredentialsEntity, DeviceCredentials> implements DeviceCredentialsDao {
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
index b326fcb..23b3fc9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
@@ -15,16 +15,25 @@
  */
 package org.thingsboard.server.dao.device;
 
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.mapping.Result;
+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.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.CassandraAbstractSearchTextDao;
 import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.TenantDeviceTypeEntity;
 import org.thingsboard.server.dao.model.nosql.DeviceEntity;
 
+import javax.annotation.Nullable;
 import java.util.*;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
@@ -32,6 +41,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEntity, Device> implements DeviceDao {
 
     @Override
@@ -55,6 +65,16 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
     }
 
     @Override
+    public List<Device> findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
+        log.debug("Try to find devices by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink);
+        List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type),
+                        eq(DEVICE_TENANT_ID_PROPERTY, tenantId)), pageLink);
+        log.trace("Found devices [{}] by tenantId [{}], type [{}] and pageLink [{}]", deviceEntities, tenantId, type, pageLink);
+        return DaoUtil.convertDataList(deviceEntities);
+    }
+
+    @Override
     public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
         log.debug("Try to find devices by tenantId [{}] and device Ids [{}]", tenantId, deviceIds);
         Select select = select().from(getColumnFamilyName());
@@ -77,6 +97,19 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
     }
 
     @Override
+    public List<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
+        log.debug("Try to find devices by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", tenantId, customerId, type, pageLink);
+        List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type),
+                        eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId),
+                        eq(DEVICE_TENANT_ID_PROPERTY, tenantId)),
+                pageLink);
+
+        log.trace("Found devices [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", deviceEntities, tenantId, customerId, type, pageLink);
+        return DaoUtil.convertDataList(deviceEntities);
+    }
+
+    @Override
     public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
         log.debug("Try to find devices by tenantId [{}], customerId [{}] and device Ids [{}]", tenantId, customerId, deviceIds);
         Select select = select().from(getColumnFamilyName());
@@ -96,4 +129,37 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
         return Optional.ofNullable(DaoUtil.getData(findOneByStatement(query)));
     }
 
+    @Override
+    public ListenableFuture<List<TenantDeviceType>> findTenantDeviceTypesAsync() {
+        Select statement = select().distinct().column(DEVICE_TYPE_PROPERTY).column(DEVICE_TENANT_ID_PROPERTY).from(DEVICE_TYPES_BY_TENANT_VIEW_NAME);
+        statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
+        ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+        ListenableFuture<List<TenantDeviceTypeEntity>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<TenantDeviceTypeEntity>>() {
+            @Nullable
+            @Override
+            public List<TenantDeviceTypeEntity> apply(@Nullable ResultSet resultSet) {
+                Result<TenantDeviceTypeEntity> result = cluster.getMapper(TenantDeviceTypeEntity.class).map(resultSet);
+                if (result != null) {
+                    return result.all();
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        });
+        return Futures.transform(result, new Function<List<TenantDeviceTypeEntity>, List<TenantDeviceType>>() {
+            @Nullable
+            @Override
+            public List<TenantDeviceType> apply(@Nullable List<TenantDeviceTypeEntity> entityList) {
+                List<TenantDeviceType> list = Collections.emptyList();
+                if (entityList != null && !entityList.isEmpty()) {
+                    list = new ArrayList<>();
+                    for (TenantDeviceTypeEntity object : entityList) {
+                        list.add(object.toTenantDeviceType());
+                    }
+                }
+                return list;
+            }
+        });
+    }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
index 86d2f8f..3654ca3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
@@ -17,8 +17,10 @@ package org.thingsboard.server.dao.device;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.TenantDeviceTypeEntity;
 
 import java.util.List;
 import java.util.Optional;
@@ -48,6 +50,16 @@ public interface DeviceDao extends Dao<Device> {
     List<Device> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink);
 
     /**
+     * Find devices by tenantId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of device objects
+     */
+    List<Device> findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink);
+
+    /**
      * Find devices by tenantId and devices Ids.
      *
      * @param tenantId the tenantId
@@ -67,6 +79,18 @@ public interface DeviceDao extends Dao<Device> {
     List<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
 
     /**
+     * Find devices by tenantId, customerId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param customerId the customerId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of device objects
+     */
+    List<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink);
+
+
+    /**
      * Find devices by tenantId, customerId and devices Ids.
      *
      * @param tenantId the tenantId
@@ -84,4 +108,11 @@ public interface DeviceDao extends Dao<Device> {
      * @return the optional device object
      */
     Optional<Device> findDevicesByTenantIdAndName(UUID tenantId, String name);
+
+    /**
+     * Find tenants device types.
+     *
+     * @return the list of tenant device type objects
+     */
+    ListenableFuture<List<TenantDeviceType>> findTenantDeviceTypesAsync();
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java
new file mode 100644
index 0000000..eb9d9de
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java
@@ -0,0 +1,46 @@
+/**
+ * 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.device;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntityTypeFilter;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+public class DeviceSearchQuery {
+
+    private RelationsSearchParameters parameters;
+    @Nullable
+    private String relationType;
+    @Nullable
+    private List<String> deviceTypes;
+
+    public EntityRelationsQuery toEntitySearchQuery() {
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(parameters);
+        query.setFilters(
+                Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
+                        Collections.singletonList(EntityType.DEVICE))));
+        return query;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
index 35d3496..3e7b6af 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -44,13 +45,22 @@ public interface DeviceService {
 
     TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
 
+    TextPageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
 
     void deleteDevicesByTenantId(TenantId tenantId);
 
     TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
 
+    TextPageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
 
     void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
+
+    ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query);
+
+    ListenableFuture<List<TenantDeviceType>> findDeviceTypesByTenantId(TenantId tenantId);
+
 }
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 819f3ae..fe1997e 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
@@ -15,41 +15,48 @@
  */
 package org.thingsboard.server.dao.device;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.AsyncFunction;
+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.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
-import org.thingsboard.server.common.data.Customer;
-import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.*;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
 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.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.TenantDeviceTypeEntity;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.tenant.TenantDao;
 
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
-import static org.thingsboard.server.dao.service.Validator.validateId;
-import static org.thingsboard.server.dao.service.Validator.validateIds;
-import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+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;
@@ -140,6 +147,16 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
     }
 
     @Override
+    public TextPageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink) {
+        log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<Device> devices = deviceDao.findDevicesByTenantIdAndType(tenantId.getId(), type, pageLink);
+        return new TextPageData<>(devices, pageLink);
+    }
+
+    @Override
     public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
         log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
         validateId(tenantId, "Incorrect tenantId " + tenantId);
@@ -166,6 +183,17 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
     }
 
     @Override
+    public TextPageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink) {
+        log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<Device> devices =  deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
+        return new TextPageData<>(devices, pageLink);
+    }
+
+    @Override
     public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
         log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);
         validateId(tenantId, "Incorrect tenantId " + tenantId);
@@ -183,6 +211,51 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
         new CustomerDevicesUnassigner(tenantId).removeEntitites(customerId);
     }
 
+    @Override
+    public ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query) {
+        ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
+        ListenableFuture<List<Device>> devices = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Device>>) relations1 -> {
+            EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
+            List<ListenableFuture<Device>> futures = new ArrayList<>();
+            for (EntityRelation relation : relations1) {
+                EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
+                if (entityId.getEntityType() == EntityType.DEVICE) {
+                    futures.add(findDeviceByIdAsync(new DeviceId(entityId.getId())));
+                }
+            }
+            return Futures.successfulAsList(futures);
+        });
+
+        devices = Futures.transform(devices, new Function<List<Device>, List<Device>>() {
+            @Nullable
+            @Override
+            public List<Device> apply(@Nullable List<Device> deviceList) {
+                return deviceList.stream().filter(device -> query.getDeviceTypes().contains(device.getType())).collect(Collectors.toList());
+            }
+        });
+
+        return devices;
+    }
+
+    @Override
+    public ListenableFuture<List<TenantDeviceType>> findDeviceTypesByTenantId(TenantId tenantId) {
+        log.trace("Executing findDeviceTypesByTenantId, tenantId [{}]", tenantId);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        ListenableFuture<List<TenantDeviceType>> tenantDeviceTypeEntities = deviceDao.findTenantDeviceTypesAsync();
+        ListenableFuture<List<TenantDeviceType>> tenantDeviceTypes = Futures.transform(tenantDeviceTypeEntities,
+            (Function<List<TenantDeviceType>, List<TenantDeviceType>>) deviceTypeEntities -> {
+                List<TenantDeviceType> deviceTypes = new ArrayList<>();
+                for (TenantDeviceType deviceType : deviceTypeEntities) {
+                    if (deviceType.getTenantId().equals(tenantId.getId())) {
+                        deviceTypes.add(deviceType);
+                    }
+                }
+                deviceTypes.sort(Comparator.comparing(TenantDeviceType::getType));
+                return deviceTypes;
+            });
+        return tenantDeviceTypes;
+    }
+
     private DataValidator<Device> deviceValidator =
             new DataValidator<Device>() {
 
@@ -208,6 +281,9 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
 
                 @Override
                 protected void validateDataImpl(Device device) {
+                    if (StringUtils.isEmpty(device.getType())) {
+                        throw new DataValidationException("Device type should be specified!");
+                    }
                     if (StringUtils.isEmpty(device.getName())) {
                         throw new DataValidationException("Device name should be specified!");
                     }
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/event/CassandraBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
index 5d4b91c..4d9da21 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
@@ -22,6 +22,7 @@ import com.datastax.driver.core.querybuilder.Select;
 import com.datastax.driver.core.utils.UUIDs;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Event;
 import org.thingsboard.server.common.data.id.EntityId;
@@ -44,6 +45,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventEntity, Event> implements EventDao {
 
     private final TenantId systemTenantId = new TenantId(NULL_UUID);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index ff2a37a..3fd57c5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -121,11 +121,15 @@ public class ModelConstants {
     public static final String DEVICE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
     public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
     public static final String DEVICE_NAME_PROPERTY = "name";
+    public static final String DEVICE_TYPE_PROPERTY = "type";
     public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
 
     public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
+    public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";
     public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
+    public static final String DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_by_type_and_search_text";
     public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
+    public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant";
 
     /**
      * Cassandra asset constants.
@@ -138,8 +142,11 @@ public class ModelConstants {
     public static final String ASSET_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
 
     public static final String ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_and_search_text";
+    public static final String ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_by_type_and_search_text";
     public static final String ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_customer_and_search_text";
+    public static final String ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_customer_by_type_and_search_text";
     public static final String ASSET_BY_TENANT_AND_NAME_VIEW_NAME = "asset_by_tenant_and_name";
+    public static final String ASSET_TYPES_BY_TENANT_VIEW_NAME = "asset_types_by_tenant";
 
     /**
      * Cassandra alarm constants.
@@ -169,7 +176,9 @@ public class ModelConstants {
     public static final String RELATION_TO_ID_PROPERTY = "to_id";
     public static final String RELATION_TO_TYPE_PROPERTY = "to_type";
     public static final String RELATION_TYPE_PROPERTY = "relation_type";
+    public static final String RELATION_TYPE_GROUP_PROPERTY = "relation_type_group";
 
+    public static final String RELATION_BY_TYPE_AND_CHILD_TYPE_VIEW_NAME = "relation_by_type_and_child_type";
     public static final String RELATION_REVERSE_VIEW_NAME = "reverse_relation";
 
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
index a6cc617..2f82e03 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
@@ -50,12 +50,13 @@ public final class AssetEntity implements SearchTextEntity<Asset> {
     @Column(name = ASSET_CUSTOMER_ID_PROPERTY)
     private UUID customerId;
 
-    @Column(name = ASSET_NAME_PROPERTY)
-    private String name;
-
+    @PartitionKey(value = 3)
     @Column(name = ASSET_TYPE_PROPERTY)
     private String type;
 
+    @Column(name = ASSET_NAME_PROPERTY)
+    private String name;
+
     @Column(name = SEARCH_TEXT_PROPERTY)
     private String searchText;
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
index 8b21096..3d54db6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
@@ -50,9 +50,13 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
     @Column(name = DEVICE_CUSTOMER_ID_PROPERTY)
     private UUID customerId;
 
+    @PartitionKey(value = 3)
+    @Column(name = DEVICE_TYPE_PROPERTY)
+    private String type;
+
     @Column(name = DEVICE_NAME_PROPERTY)
     private String name;
-    
+
     @Column(name = SEARCH_TEXT_PROPERTY)
     private String searchText;
     
@@ -74,6 +78,7 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
             this.customerId = device.getCustomerId().getId();
         }
         this.name = device.getName();
+        this.type = device.getType();
         this.additionalInfo = device.getAdditionalInfo();
     }
     
@@ -109,6 +114,14 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
         this.name = name;
     }
 
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
     public JsonNode getAdditionalInfo() {
         return additionalInfo;
     }
@@ -139,6 +152,7 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
         result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
         result = prime * result + ((id == null) ? 0 : id.hashCode());
         result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((type == null) ? 0 : type.hashCode());
         result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
         return result;
     }
@@ -172,6 +186,11 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
                 return false;
         } else if (!name.equals(other.name))
             return false;
+        if (type == null) {
+            if (other.type != null)
+                return false;
+        } else if (!type.equals(other.type))
+            return false;
         if (tenantId == null) {
             if (other.tenantId != null)
                 return false;
@@ -191,6 +210,8 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
         builder.append(customerId);
         builder.append(", name=");
         builder.append(name);
+        builder.append(", type=");
+        builder.append(type);
         builder.append(", additionalInfo=");
         builder.append(additionalInfo);
         builder.append("]");
@@ -208,6 +229,7 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
             device.setCustomerId(new CustomerId(customerId));
         }
         device.setName(name);
+        device.setType(type);
         device.setAdditionalInfo(additionalInfo);
         return device;
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/TenantAssetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/TenantAssetTypeEntity.java
new file mode 100644
index 0000000..36361ef
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/TenantAssetTypeEntity.java
@@ -0,0 +1,107 @@
+/**
+ * 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.model;
+
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = ASSET_TYPES_BY_TENANT_VIEW_NAME)
+public class TenantAssetTypeEntity {
+
+    @Transient
+    private static final long serialVersionUID = -1268181161886910152L;
+
+    @PartitionKey(value = 0)
+    @Column(name = ASSET_TYPE_PROPERTY)
+    private String type;
+
+    @PartitionKey(value = 1)
+    @Column(name = ASSET_TENANT_ID_PROPERTY)
+    private UUID tenantId;
+
+    public TenantAssetTypeEntity() {
+        super();
+    }
+
+    public TenantAssetTypeEntity(TenantAssetType tenantAssetType) {
+        this.type = tenantAssetType.getType();
+        if (tenantAssetType.getTenantId() != null) {
+            this.tenantId = tenantAssetType.getTenantId().getId();
+        }
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public UUID getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(UUID tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantAssetTypeEntity that = (TenantAssetTypeEntity) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantAssetTypeEntity{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    public TenantAssetType toTenantAssetType() {
+        TenantAssetType tenantAssetType = new TenantAssetType();
+        tenantAssetType.setType(type);
+        if (tenantId != null) {
+            tenantAssetType.setTenantId(new TenantId(tenantId));
+        }
+        return tenantAssetType;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/TenantDeviceTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/TenantDeviceTypeEntity.java
new file mode 100644
index 0000000..dad954c
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/TenantDeviceTypeEntity.java
@@ -0,0 +1,107 @@
+/**
+ * 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.model;
+
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import org.thingsboard.server.common.data.TenantDeviceType;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = DEVICE_TYPES_BY_TENANT_VIEW_NAME)
+public class TenantDeviceTypeEntity {
+
+    @Transient
+    private static final long serialVersionUID = -1268181166886910152L;
+
+    @PartitionKey(value = 0)
+    @Column(name = DEVICE_TYPE_PROPERTY)
+    private String type;
+
+    @PartitionKey(value = 1)
+    @Column(name = DEVICE_TENANT_ID_PROPERTY)
+    private UUID tenantId;
+
+    public TenantDeviceTypeEntity() {
+        super();
+    }
+
+    public TenantDeviceTypeEntity(TenantDeviceType tenantDeviceType) {
+        this.type = tenantDeviceType.getType();
+        if (tenantDeviceType.getTenantId() != null) {
+            this.tenantId = tenantDeviceType.getTenantId().getId();
+        }
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public UUID getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(UUID tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantDeviceTypeEntity that = (TenantDeviceTypeEntity) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantDeviceTypeEntity{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    public TenantDeviceType toTenantDeviceType() {
+        TenantDeviceType tenantDeviceType = new TenantDeviceType();
+        tenantDeviceType.setType(type);
+        if (tenantId != null) {
+            tenantDeviceType.setTenantId(new TenantId(tenantId));
+        }
+        return tenantDeviceType;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java
new file mode 100644
index 0000000..1c0514e
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.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.model.type;
+
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+
+public class RelationTypeGroupCodec extends EnumNameCodec<RelationTypeGroup> {
+
+    public RelationTypeGroupCodec() {
+        super(RelationTypeGroup.class);
+    }
+
+}
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 5784840..342ca4a 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;
@@ -32,7 +31,7 @@ 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;
@@ -50,7 +49,7 @@ import java.util.stream.Collectors;
 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/plugin/CassandraBasePluginDao.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/CassandraBasePluginDao.java
index 9c196e8..efcbacd 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/plugin/CassandraBasePluginDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/CassandraBasePluginDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.plugin;
 
 import com.datastax.driver.core.querybuilder.Select;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -36,6 +37,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraBasePluginDao extends CassandraAbstractSearchTextDao<PluginMetaDataEntity, PluginMetaData> implements PluginDao {
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
index d3a886b..ff4b2f8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -16,28 +16,41 @@
 package org.thingsboard.server.dao.relation;
 
 import com.datastax.driver.core.*;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
 import com.fasterxml.jackson.databind.JsonNode;
 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.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.dao.CassandraAbstractAsyncDao;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.CassandraAbstractSearchTimeDao;
 import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.type.RelationTypeGroupCodec;
 
 import javax.annotation.Nullable;
 import javax.annotation.PostConstruct;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_COLUMN_FAMILY_NAME;
+
 /**
  * Created by ashvayka on 25.04.17.
  */
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class BaseRelationDao extends CassandraAbstractAsyncDao implements RelationDao {
 
     private static final String SELECT_COLUMNS = "SELECT " +
@@ -45,12 +58,15 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
             ModelConstants.RELATION_FROM_TYPE_PROPERTY + "," +
             ModelConstants.RELATION_TO_ID_PROPERTY + "," +
             ModelConstants.RELATION_TO_TYPE_PROPERTY + "," +
+            ModelConstants.RELATION_TYPE_GROUP_PROPERTY + "," +
             ModelConstants.RELATION_TYPE_PROPERTY + "," +
             ModelConstants.ADDITIONAL_INFO_PROPERTY;
     public static final String FROM = " FROM ";
     public static final String WHERE = " WHERE ";
     public static final String AND = " AND ";
 
+    private static final RelationTypeGroupCodec relationTypeGroupCodec = new RelationTypeGroupCodec();
+
     private PreparedStatement saveStmt;
     private PreparedStatement findAllByFromStmt;
     private PreparedStatement findAllByFromAndTypeStmt;
@@ -66,43 +82,52 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from) {
-        BoundStatement stmt = getFindAllByFromStmt().bind().setUUID(0, from.getId()).setString(1, from.getEntityType().name());
+    public ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from, RelationTypeGroup typeGroup) {
+        BoundStatement stmt = getFindAllByFromStmt().bind()
+                .setUUID(0, from.getId())
+                .setString(1, from.getEntityType().name())
+                .set(2, typeGroup, relationTypeGroupCodec);
         return executeAsyncRead(from, stmt);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType) {
+    public ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getFindAllByFromAndTypeStmt().bind()
                 .setUUID(0, from.getId())
                 .setString(1, from.getEntityType().name())
-                .setString(2, relationType);
+                .set(2, typeGroup, relationTypeGroupCodec)
+                .setString(3, relationType);
         return executeAsyncRead(from, stmt);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to) {
-        BoundStatement stmt = getFindAllByToStmt().bind().setUUID(0, to.getId()).setString(1, to.getEntityType().name());
+    public ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to, RelationTypeGroup typeGroup) {
+        BoundStatement stmt = getFindAllByToStmt().bind()
+                .setUUID(0, to.getId())
+                .setString(1, to.getEntityType().name())
+                .set(2, typeGroup, relationTypeGroupCodec);
         return executeAsyncRead(to, stmt);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType) {
+    public ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getFindAllByToAndTypeStmt().bind()
                 .setUUID(0, to.getId())
                 .setString(1, to.getEntityType().name())
-                .setString(2, relationType);
+                .set(2, typeGroup, relationTypeGroupCodec)
+                .setString(3, relationType);
         return executeAsyncRead(to, stmt);
     }
 
     @Override
-    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType) {
+    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getCheckRelationStmt().bind()
                 .setUUID(0, from.getId())
                 .setString(1, from.getEntityType().name())
                 .setUUID(2, to.getId())
                 .setString(3, to.getEntityType().name())
-                .setString(4, relationType);
+                .set(4, typeGroup, relationTypeGroupCodec)
+                .setString(5, relationType);
         return getFuture(executeAsyncRead(stmt), rs -> rs != null ? rs.one() != null : false);
     }
 
@@ -113,25 +138,27 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
                 .setString(1, relation.getFrom().getEntityType().name())
                 .setUUID(2, relation.getTo().getId())
                 .setString(3, relation.getTo().getEntityType().name())
-                .setString(4, relation.getType())
-                .set(5, relation.getAdditionalInfo(), JsonNode.class);
+                .set(4, relation.getTypeGroup(), relationTypeGroupCodec)
+                .setString(5, relation.getType())
+                .set(6, relation.getAdditionalInfo(), JsonNode.class);
         ResultSetFuture future = executeAsyncWrite(stmt);
         return getBooleanListenableFuture(future);
     }
 
     @Override
     public ListenableFuture<Boolean> deleteRelation(EntityRelation relation) {
-        return deleteRelation(relation.getFrom(), relation.getTo(), relation.getType());
+        return deleteRelation(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
     }
 
     @Override
-    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType) {
+    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getDeleteStmt().bind()
                 .setUUID(0, from.getId())
                 .setString(1, from.getEntityType().name())
                 .setUUID(2, to.getId())
                 .setString(3, to.getEntityType().name())
-                .setString(4, relationType);
+                .set(4, typeGroup, relationTypeGroupCodec)
+                .setString(5, relationType);
         ResultSetFuture future = executeAsyncWrite(stmt);
         return getBooleanListenableFuture(future);
     }
@@ -145,6 +172,21 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
         return getBooleanListenableFuture(future);
     }
 
+    @Override
+    public ListenableFuture<List<EntityRelation>> findRelations(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType childType, TimePageLink pageLink) {
+        Select.Where query = CassandraAbstractSearchTimeDao.buildQuery(ModelConstants.RELATION_BY_TYPE_AND_CHILD_TYPE_VIEW_NAME,
+                Arrays.asList(eq(ModelConstants.RELATION_FROM_ID_PROPERTY, from.getId()),
+                        eq(ModelConstants.RELATION_FROM_TYPE_PROPERTY, from.getEntityType().name()),
+                        eq(ModelConstants.RELATION_TYPE_GROUP_PROPERTY, typeGroup.name()),
+                        eq(ModelConstants.RELATION_TYPE_PROPERTY, relationType),
+                        eq(ModelConstants.RELATION_TO_TYPE_PROPERTY, childType.name())),
+                Arrays.asList(QueryBuilder.asc(ModelConstants.RELATION_TYPE_GROUP_PROPERTY),
+                        QueryBuilder.asc(ModelConstants.RELATION_TYPE_PROPERTY),
+                        QueryBuilder.asc(ModelConstants.RELATION_TO_TYPE_PROPERTY)),
+                pageLink, ModelConstants.RELATION_TO_ID_PROPERTY);
+        return getFuture(executeAsyncRead(query), rs -> getEntityRelations(rs));
+    }
+
     private PreparedStatement getSaveStmt() {
         if (saveStmt == null) {
             saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
@@ -152,9 +194,10 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
                     "," + ModelConstants.RELATION_FROM_TYPE_PROPERTY +
                     "," + ModelConstants.RELATION_TO_ID_PROPERTY +
                     "," + ModelConstants.RELATION_TO_TYPE_PROPERTY +
+                    "," + ModelConstants.RELATION_TYPE_GROUP_PROPERTY +
                     "," + ModelConstants.RELATION_TYPE_PROPERTY +
                     "," + ModelConstants.ADDITIONAL_INFO_PROPERTY + ")" +
-                    " VALUES(?, ?, ?, ?, ?, ?)");
+                    " VALUES(?, ?, ?, ?, ?, ?, ?)");
         }
         return saveStmt;
     }
@@ -166,6 +209,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ?" +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ?");
         }
         return deleteStmt;
@@ -185,7 +229,8 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
             findAllByFromStmt = getSession().prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " +
-                    AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? ");
+                    AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? ");
         }
         return findAllByFromStmt;
     }
@@ -196,17 +241,20 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
                     FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? ");
         }
         return findAllByFromAndTypeStmt;
     }
 
+
     private PreparedStatement getFindAllByToStmt() {
         if (findAllByToStmt == null) {
             findAllByToStmt = getSession().prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
                     WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " +
-                    AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? ");
+                    AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? ");
         }
         return findAllByToStmt;
     }
@@ -217,11 +265,13 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
                     FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
                     WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? ");
         }
         return findAllByToAndTypeStmt;
     }
 
+
     private PreparedStatement getCheckRelationStmt() {
         if (checkRelationStmt == null) {
             checkRelationStmt = getSession().prepare(SELECT_COLUMNS + " " +
@@ -230,36 +280,19 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? ");
         }
         return checkRelationStmt;
     }
 
-    private EntityRelation getEntityRelation(Row row) {
-        EntityRelation relation = new EntityRelation();
-        relation.setType(row.getString(ModelConstants.RELATION_TYPE_PROPERTY));
-        relation.setAdditionalInfo(row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY, JsonNode.class));
-        relation.setFrom(toEntity(row, ModelConstants.RELATION_FROM_ID_PROPERTY, ModelConstants.RELATION_FROM_TYPE_PROPERTY));
-        relation.setTo(toEntity(row, ModelConstants.RELATION_TO_ID_PROPERTY, ModelConstants.RELATION_TO_TYPE_PROPERTY));
-        return relation;
-    }
-
     private EntityId toEntity(Row row, String uuidColumn, String typeColumn) {
         return EntityIdFactory.getByTypeAndUuid(row.getString(typeColumn), row.getUUID(uuidColumn));
     }
 
     private ListenableFuture<List<EntityRelation>> executeAsyncRead(EntityId from, BoundStatement stmt) {
         log.debug("Generated query [{}] for entity {}", stmt, from);
-        return getFuture(executeAsyncRead(stmt), rs -> {
-            List<Row> rows = rs.all();
-            List<EntityRelation> entries = new ArrayList<>(rows.size());
-            if (!rows.isEmpty()) {
-                rows.forEach(row -> {
-                    entries.add(getEntityRelation(row));
-                });
-            }
-            return entries;
-        });
+        return getFuture(executeAsyncRead(stmt), rs -> getEntityRelations(rs));
     }
 
     private ListenableFuture<Boolean> getBooleanListenableFuture(ResultSetFuture rsFuture) {
@@ -276,4 +309,25 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
         }, readResultsProcessingExecutor);
     }
 
+    private List<EntityRelation> getEntityRelations(ResultSet rs) {
+        List<Row> rows = rs.all();
+        List<EntityRelation> entries = new ArrayList<>(rows.size());
+        if (!rows.isEmpty()) {
+            rows.forEach(row -> {
+                entries.add(getEntityRelation(row));
+            });
+        }
+        return entries;
+    }
+
+    private EntityRelation getEntityRelation(Row row) {
+        EntityRelation relation = new EntityRelation();
+        relation.setTypeGroup(row.get(ModelConstants.RELATION_TYPE_GROUP_PROPERTY, relationTypeGroupCodec));
+        relation.setType(row.getString(ModelConstants.RELATION_TYPE_PROPERTY));
+        relation.setAdditionalInfo(row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY, JsonNode.class));
+        relation.setFrom(toEntity(row, ModelConstants.RELATION_FROM_ID_PROPERTY, ModelConstants.RELATION_FROM_TYPE_PROPERTY));
+        relation.setTo(toEntity(row, ModelConstants.RELATION_TO_ID_PROPERTY, ModelConstants.RELATION_TO_TYPE_PROPERTY));
+        return relation;
+    }
+
 }
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..9559cd3 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,25 @@ 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.common.data.relation.RelationTypeGroup;
+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,11 +57,14 @@ 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);
-        validate(from, to, relationType);
-        return relationDao.checkRelation(from, to, relationType);
+    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        log.trace("Executing checkRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
+        validate(from, to, relationType, typeGroup);
+        return relationDao.checkRelation(from, to, relationType, typeGroup);
     }
 
     @Override
@@ -63,23 +82,28 @@ public class BaseRelationService implements RelationService {
     }
 
     @Override
-    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType) {
-        log.trace("Executing deleteRelation [{}][{}][{}]", from, to, relationType);
-        validate(from, to, relationType);
-        return relationDao.deleteRelation(from, to, relationType);
+    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
+        validate(from, to, relationType, typeGroup);
+        return relationDao.deleteRelation(from, to, relationType, typeGroup);
     }
 
     @Override
     public ListenableFuture<Boolean> deleteEntityRelations(EntityId entity) {
         log.trace("Executing deleteEntityRelations [{}]", entity);
         validate(entity);
-        ListenableFuture<List<EntityRelation>> inboundRelations = relationDao.findAllByTo(entity);
-        ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction<List<EntityRelation>, List<Boolean>>() {
+        List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
+        for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
+            inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
+        }
+        Futures.allAsList(inboundRelationsList);
+        ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
+        ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction<List<List<EntityRelation>>, List<Boolean>>() {
             @Override
-            public ListenableFuture<List<Boolean>> apply(List<EntityRelation> relations) throws Exception {
+            public ListenableFuture<List<Boolean>> apply(List<List<EntityRelation>> relations) throws Exception {
                 List<ListenableFuture<Boolean>> results = new ArrayList<>();
-                for (EntityRelation relation : relations) {
-                    results.add(relationDao.deleteRelation(relation));
+                for (List<EntityRelation> relationList : relations) {
+                    relationList.stream().forEach(relation -> results.add(relationDao.deleteRelation(relation)));
                 }
                 return Futures.allAsList(results);
             }
@@ -93,33 +117,63 @@ public class BaseRelationService implements RelationService {
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByFrom(EntityId from) {
-        log.trace("Executing findByFrom [{}]", from);
+    public ListenableFuture<List<EntityRelation>> findByFrom(EntityId from, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByFrom [{}][{}]", from, typeGroup);
         validate(from);
-        return relationDao.findAllByFrom(from);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByFrom(from, typeGroup);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType) {
-        log.trace("Executing findByFromAndType [{}][{}]", from, relationType);
+    public ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from, RelationTypeGroup typeGroup) {
+        log.trace("Executing findInfoByFrom [{}][{}]", from, typeGroup);
+        validate(from);
+        validateTypeGroup(typeGroup);
+        ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByFrom(from, typeGroup);
+        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, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup);
         validate(from);
         validateType(relationType);
-        return relationDao.findAllByFromAndType(from, relationType);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByFromAndType(from, relationType, typeGroup);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByTo(EntityId to) {
-        log.trace("Executing findByTo [{}]", to);
+    public ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByTo [{}][{}]", to, typeGroup);
         validate(to);
-        return relationDao.findAllByTo(to);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByTo(to, typeGroup);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType) {
-        log.trace("Executing findByToAndType [{}][{}]", to, relationType);
+    public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup);
         validate(to);
         validateType(relationType);
-        return relationDao.findAllByToAndType(to, relationType);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByToAndType(to, relationType, typeGroup);
     }
 
     @Override
@@ -161,11 +215,12 @@ public class BaseRelationService implements RelationService {
         if (relation == null) {
             throw new DataValidationException("Relation type should be specified!");
         }
-        validate(relation.getFrom(), relation.getTo(), relation.getType());
+        validate(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
     }
 
-    protected void validate(EntityId from, EntityId to, String type) {
+    protected void validate(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) {
         validateType(type);
+        validateTypeGroup(typeGroup);
         if (from == null) {
             throw new DataValidationException("Relation should contain from entity!");
         }
@@ -180,6 +235,12 @@ public class BaseRelationService implements RelationService {
         }
     }
 
+    private void validateTypeGroup(RelationTypeGroup typeGroup) {
+        if (typeGroup == null) {
+            throw new DataValidationException("Relation type group should be specified!");
+        }
+    }
+
     protected void validate(EntityId entity) {
         if (entity == null) {
             throw new DataValidationException("Entity should be specified!");
@@ -251,9 +312,9 @@ public class BaseRelationService implements RelationService {
     private ListenableFuture<List<EntityRelation>> findRelations(final EntityId rootId, final EntitySearchDirection direction) {
         ListenableFuture<List<EntityRelation>> relations;
         if (direction == EntitySearchDirection.FROM) {
-            relations = findByFrom(rootId);
+            relations = findByFrom(rootId, RelationTypeGroup.COMMON);
         } else {
-            relations = findByTo(rootId);
+            relations = findByTo(rootId, RelationTypeGroup.COMMON);
         }
         return relations;
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
index df47259..3abadb8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
@@ -16,8 +16,11 @@
 package org.thingsboard.server.dao.relation;
 
 import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 
 import java.util.List;
 
@@ -26,22 +29,24 @@ import java.util.List;
  */
 public interface RelationDao {
 
-    ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from);
+    ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType);
+    ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to);
+    ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType);
+    ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup);
 
-    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> saveRelation(EntityRelation relation);
 
     ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
 
-    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> deleteOutboundRelations(EntityId entity);
 
+    ListenableFuture<List<EntityRelation>> findRelations(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType toType, TimePageLink pageLink);
+
 }
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..868769f 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,8 @@ 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 org.thingsboard.server.common.data.relation.RelationTypeGroup;
 
 import java.util.List;
 
@@ -26,23 +28,25 @@ import java.util.List;
  */
 public interface RelationService {
 
-    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> saveRelation(EntityRelation relation);
 
     ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
 
-    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> deleteEntityRelations(EntityId entity);
 
-    ListenableFuture<List<EntityRelation>> findByFrom(EntityId from);
+    ListenableFuture<List<EntityRelation>> findByFrom(EntityId from, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType);
+    ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findByTo(EntityId to);
+    ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType);
+    ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup);
+
+    ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);
 
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 f7c4f6a..a5b34a2 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
@@ -32,7 +32,7 @@ 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;
@@ -52,7 +52,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);
 
@@ -243,7 +243,6 @@ public class BaseRuleService extends BaseEntityService implements RuleService {
     @Override
     public void activateRuleById(RuleId ruleId) {
         updateLifeCycleState(ruleId, ComponentLifecycleState.ACTIVE);
-
     }
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraBaseRuleDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraBaseRuleDao.java
index 8f62baf..962cd59 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraBaseRuleDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraBaseRuleDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.rule;
 
 import com.datastax.driver.core.querybuilder.Select;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -36,6 +37,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraBaseRuleDao extends CassandraAbstractSearchTextDao<RuleMetaDataEntity, RuleMetaData> implements RuleDao {
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
index adeeed0..a69d30f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.settings;
 
 import com.datastax.driver.core.querybuilder.Select.Where;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.AdminSettings;
 import org.thingsboard.server.dao.CassandraAbstractModelDao;
@@ -29,6 +30,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraAdminSettingsDao extends CassandraAbstractModelDao<AdminSettingsEntity, AdminSettings> implements AdminSettingsDao {
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
index f14a08f..e46c144 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
@@ -27,7 +27,7 @@ import java.util.UUID;
  * Created by Valerii Sosliuk on 5/21/2017.
  */
 @ConditionalOnProperty(prefix = "sql", value = "enabled", havingValue = "true", matchIfMissing = false)
-public interface AlarmRepository extends CrudRepository<AlarmEntity, Alarm> {
+public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
 
     @Query(nativeQuery = true, value = "SELECT * FROM ALARM WHERE TENANT_ID = ?1 AND ORIGINATOR_ID = ?2 " +
             "AND ?3 = ?3 AND TYPE = ?4 ORDER BY ID DESC LIMIT 1")
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
index f5d69cd..cd0df63 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
@@ -15,22 +15,30 @@
  */
 package org.thingsboard.server.dao.sql.alarm;
 
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.*;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.data.repository.CrudRepository;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.DaoUtil;
 import org.thingsboard.server.dao.alarm.AlarmDao;
+import org.thingsboard.server.dao.alarm.BaseAlarmService;
 import org.thingsboard.server.dao.model.sql.AlarmEntity;
+import org.thingsboard.server.dao.relation.RelationDao;
 import org.thingsboard.server.dao.sql.JpaAbstractDao;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.Executors;
 
 import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW;
@@ -38,6 +46,7 @@ import static org.springframework.transaction.annotation.Propagation.REQUIRES_NE
 /**
  * Created by Valerii Sosliuk on 5/19/2017.
  */
+@Slf4j
 @Component
 @ConditionalOnProperty(prefix = "sql", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements AlarmDao {
@@ -45,6 +54,9 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
     @Autowired
     private AlarmRepository alarmRepository;
 
+    @Autowired
+    private RelationDao relationDao;
+
     @Override
     protected Class getEntityClass() {
         return AlarmEntity.class;
@@ -59,9 +71,28 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
     @Transactional(propagation = REQUIRES_NEW)
     public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
         ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
-        ListenableFuture<Alarm> listenableFuture = service.submit(() -> DaoUtil.getData(
+        return service.submit(() -> DaoUtil.getData(
                 alarmRepository.findLatestByOriginatorAndType(tenantId.getId(), originator.getId(),
                 originator.getEntityType().ordinal(), type)));
-        return listenableFuture;
+    }
+
+    @Override
+    public ListenableFuture<Alarm> findAlarmByIdAsync(UUID key) {
+        return findByIdAsync(key);
+    }
+
+    @Override
+    public ListenableFuture<List<Alarm>> findAlarms(AlarmQuery query) {
+        log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink());
+        EntityId affectedEntity = query.getAffectedEntityId();
+        String relationType = query.getStatus() == null ? BaseAlarmService.ALARM_RELATION : BaseAlarmService.ALARM_RELATION_PREFIX + query.getStatus().name();
+        ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
+        return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Alarm>>) input -> {
+            List<ListenableFuture<Alarm>> alarmFutures = new ArrayList<>(input.size());
+            for (EntityRelation relation : input) {
+                alarmFutures.add(findAlarmByIdAsync(relation.getTo().getId()));
+            }
+            return Futures.successfulAsList(alarmFutures);
+        });
     }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
index cf38c02..b6c9891 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
@@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.CrudRepository;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.dao.model.sql.AssetEntity;
 
 import java.util.List;
@@ -58,4 +59,19 @@ public interface AssetRepository extends CrudRepository<AssetEntity, UUID> {
     List<AssetEntity> findByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List<UUID> assetIds);
 
     AssetEntity findByTenantIdAndName(UUID tenantId, String name);
+
+    @Query(nativeQuery = true, value = "SELECT * FROM ASSET WHERE TENANT_ID = ?2 " +
+            "AND CUSTOMER_ID = ?3 AND TYPE = ?4 " +
+            "AND LOWER(SEARCH_TEXT) LIKE LOWER(CONCAT(?5, '%')) " +
+            "ORDER BY ID LIMIT ?1")
+    List<AssetEntity> findByTenantIdAndCustomerIdAndTypeFirstPage(int limit, UUID tenantId, UUID customerId, String type, String textSearch);
+
+    @Query(nativeQuery = true, value = "SELECT * FROM ASSET WHERE TENANT_ID = ?2 " +
+            "AND CUSTOMER_ID = ?3 AND TYPE = ?4 " +
+            "AND LOWER(SEARCH_TEXT) LIKE LOWER(CONCAT(?5, '%')) " +
+            "AND ID > ?6 ORDER BY ID LIMIT ?1")
+    List<AssetEntity> findByTenantIdAndCustomerIdAndTypeNextPage(int limit, UUID tenantId, UUID customerId, String type, String textSearch, UUID idOffset);
+
+    @Query(value = "SELECT NEW org.thingsboard.server.common.data.asset.TenantAssetType(a.type, a.tenantId) FROM AssetEntity a GROUP BY a.tenantId, a.type")
+    List<TenantAssetType> findTenantAssetTypes();
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
index 2d301c2..49f8d44 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.data.repository.CrudRepository;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.DaoUtil;
 import org.thingsboard.server.dao.asset.AssetDao;
@@ -95,4 +96,26 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao<AssetEntity, Asset> im
         Asset asset = DaoUtil.getData(assetRepository.findByTenantIdAndName(tenantId, name));
         return Optional.ofNullable(asset);
     }
+
+    @Override
+    public List<Asset> findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
+        return null;
+    }
+
+    @Override
+    public List<Asset> findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
+        if (pageLink.getIdOffset() == null) {
+            return DaoUtil.convertDataList(assetRepository.findByTenantIdAndCustomerIdAndTypeFirstPage(pageLink.getLimit(), tenantId,
+                    customerId, type, pageLink.getTextSearch()));
+        } else {
+            return DaoUtil.convertDataList(assetRepository.findByTenantIdAndCustomerIdAndTypeNextPage(pageLink.getLimit(), tenantId,
+                    customerId, type, pageLink.getTextSearch(), pageLink.getIdOffset()));
+        }
+    }
+
+    @Override
+    public ListenableFuture<List<TenantAssetType>> findTenantAssetTypesAsync() {
+        ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
+        return service.submit(() -> assetRepository.findTenantAssetTypes());
+    }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
index c9b9c75..6790c7a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.data.repository.CrudRepository;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.DaoUtil;
 import org.thingsboard.server.dao.device.DeviceDao;
@@ -98,4 +99,22 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
         Device device = DaoUtil.getData(deviceRepository.findByTenantIdAndName(tenantId, name));
         return Optional.ofNullable(device);
     }
+
+    @Override
+    public List<Device> findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
+        //TODO
+        return null;
+    }
+
+    @Override
+    public List<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
+        //TODO
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<List<TenantDeviceType>> findTenantDeviceTypesAsync() {
+        //TODO
+        return null;
+    }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
index 7073b49..a2b2bc9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
@@ -76,7 +76,6 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D> implements Dao<
     @Override
     public ListenableFuture<D> findByIdAsync(UUID key) {
         log.debug("Get entity by key async {}", key);
-        // Should ListeningExecutorService be a field?
         ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
         ListenableFuture<D> listenableFuture = service.submit(() -> DaoUtil.getData(getCrudRepository().findOne(key)));
         return listenableFuture;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
new file mode 100644
index 0000000..beaa9c6
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
@@ -0,0 +1,84 @@
+package org.thingsboard.server.dao.sql.relation;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.relation.RelationDao;
+
+import java.util.List;
+
+/**
+ * Created by Valerii Sosliuk on 5/29/2017.
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "sql", value = "enabled", havingValue = "true", matchIfMissing = false)
+public class JpaRelationDao implements RelationDao {
+
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from, RelationTypeGroup typeGroup) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to, RelationTypeGroup typeGroup) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<Boolean> saveRelation(EntityRelation relation) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<Boolean> deleteRelation(EntityRelation relation) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<Boolean> deleteOutboundRelations(EntityId entity) {
+        // TODO: Implement
+        return null;
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findRelations(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType toType, TimePageLink pageLink) {
+        // TODO: Implement
+        return null;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
index dfefb75..e6e8db4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.dao.tenant;
 
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -31,6 +32,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraTenantDao extends CassandraAbstractSearchTextDao<TenantEntity, Tenant> implements TenantDao {
 
     @Override
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 e80af2f..7645b28 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
@@ -27,7 +27,7 @@ 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.plugin.PluginService;
 import org.thingsboard.server.dao.rule.RuleService;
@@ -43,7 +43,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
 
 @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/CassandraUserCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserCredentialsDao.java
index 22ed06a..6f94cab 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserCredentialsDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserCredentialsDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.user;
 
 import com.datastax.driver.core.querybuilder.Select.Where;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.security.UserCredentials;
 import org.thingsboard.server.dao.CassandraAbstractModelDao;
@@ -31,6 +32,7 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraUserCredentialsDao extends CassandraAbstractModelDao<UserCredentialsEntity, UserCredentials> implements UserCredentialsDao {
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserDao.java
index d09c84c..c5f11d1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/CassandraUserDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.user;
 
 import com.datastax.driver.core.querybuilder.Select.Where;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -35,6 +36,7 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraUserDao extends CassandraAbstractSearchTextDao<UserEntity, User> implements UserDao {
 
     @Override
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 92380c6..9f12288 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,8 +28,10 @@ public interface UserService {
 	
 	User findUserById(UserId userId);
 
+	ListenableFuture<User> findUserByIdAsync(UserId userId);
+
 	User findUserByEmail(String email);
-	
+
 	User saveUser(User user);
 
 	UserCredentials findUserCredentialsByUserId(UserId userId);
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 709e62d..d8782db 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
@@ -15,6 +15,15 @@
  */
 package org.thingsboard.server.dao.user;
 
+import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
+
+import java.util.List;
+
+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;
@@ -31,7 +40,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.ModelConstants;
@@ -45,7 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
 
 @Service
 @Slf4j
-public class UserServiceImpl extends BaseEntityService implements UserService {
+public class UserServiceImpl extends AbstractEntityService implements UserService {
 
     private static final int DEFAULT_TOKEN_LENGTH = 30;
 
@@ -76,6 +85,13 @@ public class UserServiceImpl extends BaseEntityService implements UserService {
 	}
 
     @Override
+    public ListenableFuture<User> findUserByIdAsync(UserId userId) {
+        log.trace("Executing findUserByIdAsync [{}]", userId);
+        validateId(userId, "Incorrect userId " + userId);
+        return userDao.findByIdAsync(userId.getId());
+    }
+
+    @Override
     public User saveUser(User user) {
         log.trace("Executing saveUser [{}]", user);
         userValidator.validate(user);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
index d43d25d..f4e7a72 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.widget;
 
 import com.datastax.driver.core.querybuilder.Select;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
@@ -33,6 +34,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraWidgetsBundleDao extends CassandraAbstractSearchTextDao<WidgetsBundleEntity, WidgetsBundle> implements WidgetsBundleDao {
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
index 8c2cae7..2f06b01 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.widget;
 
 import com.datastax.driver.core.querybuilder.Select.Where;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.dao.CassandraAbstractModelDao;
@@ -32,6 +33,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 @Component
 @Slf4j
+@ConditionalOnProperty(prefix = "cassandra", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class CassandraWidgetTypeDao extends CassandraAbstractModelDao<WidgetTypeEntity, WidgetType> implements WidgetTypeDao {
 
     @Override
diff --git a/dao/src/main/resources/demo-data.cql b/dao/src/main/resources/demo-data.cql
index 023cf1b..47d8de4 100644
--- a/dao/src/main/resources/demo-data.cql
+++ b/dao/src/main/resources/demo-data.cql
@@ -149,66 +149,73 @@ VALUES (
 
 /** Demo device **/
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+	'default',
 	'Test Device A1',
 	'test device a1'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0001' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+	'default',
 	'Test Device A2',
 	'test device a2'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0002' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+	'default',
 	'Test Device A3',
 	'test device a3'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0003' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0001' ),
+	'default',
 	'Test Device B1',
 	'test device b1'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0004' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0002' ),
+	'default',
 	'Test Device C1',
 	'test device c1'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text, additional_info)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text, additional_info)
 VALUES (
 	c8f1a6f0-b993-11e6-8a04-9ff4e1b7933c,
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( 0 ),
+	'default',
 	'DHT11 Demo Device',
 	'dht11 demo device',
 	'{"description":"Demo device that is used in sample applications that upload data from DHT11 temperature and humidity sensor"}'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text, additional_info)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text, additional_info)
 VALUES (
 	c8f1a6f0-b993-11e6-8a04-9ff4e1b7933d,
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( 0 ),
+	'default',
 	'Raspberry Pi Demo Device',
 	'raspberry pi demo device',
 	'{"description":"Demo device that is used in Raspberry Pi GPIO control sample application"}'
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
index b86031b..71de677 100644
--- a/dao/src/main/resources/schema.cql
+++ b/dao/src/main/resources/schema.cql
@@ -152,35 +152,57 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search
 	WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
 
 CREATE TABLE IF NOT EXISTS thingsboard.device (
-	id timeuuid,
-	tenant_id timeuuid,
-	customer_id timeuuid,
-	name text,
-	search_text text,
-	additional_info text,
-	PRIMARY KEY (id, tenant_id, customer_id)
+    id timeuuid,
+    tenant_id timeuuid,
+    customer_id timeuuid,
+    name text,
+    type text,
+    search_text text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id, customer_id, type)
 );
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_name AS
-	SELECT *
-	from thingsboard.device
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, name, id, customer_id)
-	WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, name, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_search_text AS
-	SELECT *
-	from thingsboard.device
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, search_text, id, customer_id)
-	WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, type, search_text, id, customer_id)
+    WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_and_search_text AS
-	SELECT *
-	from thingsboard.device
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( customer_id, tenant_id, search_text, id )
-	WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, search_text, id, type )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, type, search_text, id )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_types_by_tenant AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( (type, tenant_id), id, customer_id)
+    WITH CLUSTERING ORDER BY ( id ASC, customer_id DESC);
 
 CREATE TABLE IF NOT EXISTS thingsboard.device_credentials (
 	id timeuuid PRIMARY KEY,
@@ -202,38 +224,58 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_credent
 	WHERE credentials_id IS NOT NULL AND id IS NOT NULL
 	PRIMARY KEY ( credentials_id, id );
 
-
 CREATE TABLE IF NOT EXISTS thingsboard.asset (
-	id timeuuid,
-	tenant_id timeuuid,
-	customer_id timeuuid,
-	name text,
-	type text,
-	search_text text,
-	additional_info text,
-	PRIMARY KEY (id, tenant_id, customer_id)
+    id timeuuid,
+    tenant_id timeuuid,
+    customer_id timeuuid,
+    name text,
+    type text,
+    search_text text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id, customer_id, type)
 );
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_name AS
-	SELECT *
-	from thingsboard.asset
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, name, id, customer_id)
-	WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, name, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_search_text AS
-	SELECT *
-	from thingsboard.asset
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, search_text, id, customer_id)
-	WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, type, search_text, id, customer_id)
+    WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_and_search_text AS
-	SELECT *
-	from thingsboard.asset
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( customer_id, tenant_id, search_text, id )
-	WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, search_text, id, type )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, type, search_text, id )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_types_by_tenant AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( (type, tenant_id), id, customer_id)
+    WITH CLUSTERING ORDER BY ( id ASC, customer_id DESC);
 
 CREATE TABLE IF NOT EXISTS thingsboard.alarm (
 	id timeuuid,
@@ -265,17 +307,25 @@ CREATE TABLE IF NOT EXISTS thingsboard.relation (
 	from_type text,
 	to_id timeuuid,
 	to_type text,
+	relation_type_group text,
 	relation_type text,
 	additional_info text,
-	PRIMARY KEY ((from_id, from_type), relation_type, to_id, to_type)
-);
+	PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_id, to_type)
+) WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_id ASC, to_type ASC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.relation_by_type_and_child_type AS
+    SELECT *
+    from thingsboard.relation
+    WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
+    PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_type, to_id)
+    WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_type ASC, to_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.reverse_relation AS
-SELECT *
-from thingsboard.relation
-WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
-PRIMARY KEY ((to_id, to_type), relation_type, from_id, from_type)
-WITH CLUSTERING ORDER BY ( relation_type ASC, from_id ASC, from_type ASC);
+    SELECT *
+    from thingsboard.relation
+    WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
+    PRIMARY KEY ((to_id, to_type), relation_type_group, relation_type, from_id, from_type)
+    WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, from_id ASC, from_type ASC);
 
 CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle (
     id timeuuid,
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index d724d7f..95340e6 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -32,7 +32,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.support.AnnotationConfigContextLoader;
 import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.Event;
-import org.thingsboard.server.common.data.alarm.AlarmStatus;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UUIDBased;
@@ -42,6 +41,7 @@ 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.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -90,6 +90,9 @@ public abstract class AbstractServiceTest {
     protected DeviceService deviceService;
 
     @Autowired
+    protected AssetService assetService;
+
+    @Autowired
     protected DeviceCredentialsService deviceCredentialsService;
 
     @Autowired
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 8b90521..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
@@ -23,11 +23,14 @@ import org.junit.Test;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
 import org.thingsboard.server.common.data.alarm.AlarmStatus;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.relation.EntityRelationsQuery;
@@ -92,7 +95,78 @@ 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);
     }
+
+    @Test
+    public void testFindAlarm() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+
+        Assert.assertTrue(relationService.saveRelation(relation).get());
+
+        long ts = System.currentTimeMillis();
+        Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId)
+                .type(TEST_ALARM)
+                .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK)
+                .startTs(ts).build();
+
+        Alarm created = alarmService.createOrUpdateAlarm(alarm);
+
+        // Check child relation
+        TimePageData<Alarm> alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        // Check parent relation
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(parentId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        alarmService.ackAlarm(created.getId(), System.currentTimeMillis()).get();
+        created = alarmService.findAlarmByIdAsync(created.getId()).get();
+
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_ACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        // Check not existing relation
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(0, alarms.getData().size());
+
+        alarmService.clearAlarm(created.getId(), System.currentTimeMillis()).get();
+        created = alarmService.findAlarmByIdAsync(created.getId()).get();
+
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.CLEARED_ACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+    }
 }
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java
new file mode 100644
index 0000000..9587703
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java
@@ -0,0 +1,634 @@
+/**
+ * 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.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
+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.exception.DataValidationException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+
+public class BaseAssetServiceTest extends AbstractServiceTest {
+
+    private IdComparator<Asset> idComparator = new IdComparator<>();
+
+    private TenantId tenantId;
+
+    @Before
+    public void before() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = tenantService.saveTenant(tenant);
+        Assert.assertNotNull(savedTenant);
+        tenantId = savedTenant.getId();
+    }
+
+    @After
+    public void after() {
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testSaveAsset() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = assetService.saveAsset(asset);
+
+        Assert.assertNotNull(savedAsset);
+        Assert.assertNotNull(savedAsset.getId());
+        Assert.assertTrue(savedAsset.getCreatedTime() > 0);
+        Assert.assertEquals(asset.getTenantId(), savedAsset.getTenantId());
+        Assert.assertNotNull(savedAsset.getCustomerId());
+        Assert.assertEquals(NULL_UUID, savedAsset.getCustomerId().getId());
+        Assert.assertEquals(asset.getName(), savedAsset.getName());
+
+        savedAsset.setName("My new asset");
+
+        assetService.saveAsset(savedAsset);
+        Asset foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertEquals(foundAsset.getName(), savedAsset.getName());
+
+        assetService.deleteAsset(savedAsset.getId());
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveAssetWithEmptyName() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setType("default");
+        assetService.saveAsset(asset);
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveAssetWithEmptyTenant() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        assetService.saveAsset(asset);
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveAssetWithInvalidTenant() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        asset.setTenantId(new TenantId(UUIDs.timeBased()));
+        assetService.saveAsset(asset);
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testAssignAssetToNonExistentCustomer() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        asset.setTenantId(tenantId);
+        asset = assetService.saveAsset(asset);
+        try {
+            assetService.assignAssetToCustomer(asset.getId(), new CustomerId(UUIDs.timeBased()));
+        } finally {
+            assetService.deleteAsset(asset.getId());
+        }
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testAssignAssetToCustomerFromDifferentTenant() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        asset.setTenantId(tenantId);
+        asset = assetService.saveAsset(asset);
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Test different tenant");
+        tenant = tenantService.saveTenant(tenant);
+        Customer customer = new Customer();
+        customer.setTenantId(tenant.getId());
+        customer.setTitle("Test different customer");
+        customer = customerService.saveCustomer(customer);
+        try {
+            assetService.assignAssetToCustomer(asset.getId(), customer.getId());
+        } finally {
+            assetService.deleteAsset(asset.getId());
+            tenantService.deleteTenant(tenant.getId());
+        }
+    }
+
+    @Test
+    public void testFindAssetById() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = assetService.saveAsset(asset);
+        Asset foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertNotNull(foundAsset);
+        Assert.assertEquals(savedAsset, foundAsset);
+        assetService.deleteAsset(savedAsset.getId());
+    }
+
+    @Test
+    public void testFindAssetTypesByTenantId() throws Exception {
+        List<Asset> assets = new ArrayList<>();
+        try {
+            for (int i=0;i<3;i++) {
+                Asset asset = new Asset();
+                asset.setTenantId(tenantId);
+                asset.setName("My asset B"+i);
+                asset.setType("typeB");
+                assets.add(assetService.saveAsset(asset));
+            }
+            for (int i=0;i<7;i++) {
+                Asset asset = new Asset();
+                asset.setTenantId(tenantId);
+                asset.setName("My asset C"+i);
+                asset.setType("typeC");
+                assets.add(assetService.saveAsset(asset));
+            }
+            for (int i=0;i<9;i++) {
+                Asset asset = new Asset();
+                asset.setTenantId(tenantId);
+                asset.setName("My asset A"+i);
+                asset.setType("typeA");
+                assets.add(assetService.saveAsset(asset));
+            }
+            List<TenantAssetType> assetTypes = assetService.findAssetTypesByTenantId(tenantId).get();
+            Assert.assertNotNull(assetTypes);
+            Assert.assertEquals(3, assetTypes.size());
+            Assert.assertEquals("typeA", assetTypes.get(0).getType());
+            Assert.assertEquals("typeB", assetTypes.get(1).getType());
+            Assert.assertEquals("typeC", assetTypes.get(2).getType());
+        } finally {
+            assets.forEach((asset) -> { assetService.deleteAsset(asset.getId()); });
+        }
+    }
+
+    @Test
+    public void testDeleteAsset() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = assetService.saveAsset(asset);
+        Asset foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertNotNull(foundAsset);
+        assetService.deleteAsset(savedAsset.getId());
+        foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertNull(foundAsset);
+    }
+
+    @Test
+    public void testFindAssetsByTenantId() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Test tenant");
+        tenant = tenantService.saveTenant(tenant);
+
+        TenantId tenantId = tenant.getId();
+
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<178;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            assets.add(assetService.saveAsset(asset));
+        }
+
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+
+        assetService.deleteAssetsByTenantId(tenantId);
+
+        pageLink = new TextPageLink(33);
+        pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertTrue(pageData.getData().isEmpty());
+
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdAndName() {
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle1.add(assetService.saveAsset(asset));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle2.add(assetService.saveAsset(asset));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdAndType() {
+        String title1 = "Asset title 1";
+        String type1 = "typeA";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            assetsType1.add(assetService.saveAsset(asset));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeB";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            assetsType2.add(assetService.saveAsset(asset));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndType(tenantId, type1, pageLink);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = assetService.findAssetsByTenantIdAndType(tenantId, type2, pageLink);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndType(tenantId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndType(tenantId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdAndCustomerId() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Test tenant");
+        tenant = tenantService.saveTenant(tenant);
+
+        TenantId tenantId = tenant.getId();
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<278;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            asset = assetService.saveAsset(asset);
+            assets.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+
+        assetService.unassignCustomerAssets(tenantId, customerId);
+
+        pageLink = new TextPageLink(33);
+        pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertTrue(pageData.getData().isEmpty());
+
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdCustomerIdAndName() {
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = assetService.saveAsset(asset);
+            assetsTitle1.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = assetService.saveAsset(asset);
+            assetsTitle2.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        customerService.deleteCustomer(customerId);
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdCustomerIdAndType() {
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        String type1 = "typeC";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            asset = assetService.saveAsset(asset);
+            assetsType1.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeD";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            asset = assetService.saveAsset(asset);
+            assetsType2.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        customerService.deleteCustomer(customerId);
+    }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
index efdab8a..4d9ef9f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
@@ -58,6 +58,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithEmptyDevice() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -73,6 +74,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsType() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -88,6 +90,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsId() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -103,6 +106,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveNonExistentDeviceCredentials() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -122,6 +126,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithNonExistentDevice() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -137,6 +142,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithInvalidCredemtialsIdLength() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -153,6 +159,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
         Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
@@ -166,6 +173,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
         Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
@@ -181,6 +189,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
         Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
index b256daa..420ba6a 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
@@ -24,6 +24,7 @@ import org.junit.Test;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -35,6 +36,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executors;
 
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 
@@ -63,6 +65,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         
         Assert.assertNotNull(savedDevice);
@@ -93,6 +96,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     @Test(expected = DataValidationException.class)
     public void testSaveDeviceWithEmptyName() {
         Device device = new Device();
+        device.setType("default");
         device.setTenantId(tenantId);
         deviceService.saveDevice(device);
     }
@@ -101,6 +105,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceWithEmptyTenant() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         deviceService.saveDevice(device);
     }
     
@@ -108,6 +113,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceWithInvalidTenant() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(new TenantId(UUIDs.timeBased()));
         deviceService.saveDevice(device);
     }
@@ -116,6 +122,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testAssignDeviceToNonExistentCustomer() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         try {
@@ -129,6 +136,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testAssignDeviceToCustomerFromDifferentTenant() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         Tenant tenant = new Tenant();
@@ -151,18 +159,56 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         Device foundDevice = deviceService.findDeviceById(savedDevice.getId());
         Assert.assertNotNull(foundDevice);
         Assert.assertEquals(savedDevice, foundDevice);
         deviceService.deleteDevice(savedDevice.getId());
     }
+
+    @Test
+    public void testFindDeviceTypesByTenantId() throws Exception {
+        List<Device> devices = new ArrayList<>();
+        try {
+            for (int i=0;i<3;i++) {
+                Device device = new Device();
+                device.setTenantId(tenantId);
+                device.setName("My device B"+i);
+                device.setType("typeB");
+                devices.add(deviceService.saveDevice(device));
+            }
+            for (int i=0;i<7;i++) {
+                Device device = new Device();
+                device.setTenantId(tenantId);
+                device.setName("My device C"+i);
+                device.setType("typeC");
+                devices.add(deviceService.saveDevice(device));
+            }
+            for (int i=0;i<9;i++) {
+                Device device = new Device();
+                device.setTenantId(tenantId);
+                device.setName("My device A"+i);
+                device.setType("typeA");
+                devices.add(deviceService.saveDevice(device));
+            }
+            List<TenantDeviceType> deviceTypes = deviceService.findDeviceTypesByTenantId(tenantId).get();
+            Assert.assertNotNull(deviceTypes);
+            Assert.assertEquals(3, deviceTypes.size());
+            Assert.assertEquals("typeA", deviceTypes.get(0).getType());
+            Assert.assertEquals("typeB", deviceTypes.get(1).getType());
+            Assert.assertEquals("typeC", deviceTypes.get(2).getType());
+        } finally {
+            devices.forEach((device) -> { deviceService.deleteDevice(device.getId()); });
+        }
+    }
     
     @Test
     public void testDeleteDevice() {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         Device foundDevice = deviceService.findDeviceById(savedDevice.getId());
         Assert.assertNotNull(foundDevice);
@@ -186,6 +232,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             Device device = new Device();
             device.setTenantId(tenantId);
             device.setName("Device"+i);
+            device.setType("default");
             devices.add(deviceService.saveDevice(device));
         }
         
@@ -214,7 +261,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         
         tenantService.deleteTenant(tenantId);
     }
-    
+
     @Test
     public void testFindDevicesByTenantIdAndName() {
         String title1 = "Device title 1";
@@ -226,6 +273,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle1.add(deviceService.saveDevice(device));
         }
         String title2 = "Device title 2";
@@ -237,6 +285,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle2.add(deviceService.saveDevice(device));
         }
         
@@ -289,6 +338,85 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Assert.assertFalse(pageData.hasNext());
         Assert.assertEquals(0, pageData.getData().size());
     }
+
+    @Test
+    public void testFindDevicesByTenantIdAndType() {
+        String title1 = "Device title 1";
+        String type1 = "typeA";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            devicesType1.add(deviceService.saveDevice(device));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeB";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            devicesType2.add(deviceService.saveDevice(device));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type1, pageLink);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type2, pageLink);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
     
     @Test
     public void testFindDevicesByTenantIdAndCustomerId() {
@@ -309,6 +437,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             Device device = new Device();
             device.setTenantId(tenantId);
             device.setName("Device"+i);
+            device.setType("default");
             device = deviceService.saveDevice(device);
             devices.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
         }
@@ -357,6 +486,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = deviceService.saveDevice(device);
             devicesTitle1.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
         }
@@ -369,6 +499,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = deviceService.saveDevice(device);
             devicesTitle2.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
         }
@@ -423,4 +554,94 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Assert.assertEquals(0, pageData.getData().size());
         customerService.deleteCustomer(customerId);
     }
+
+    @Test
+    public void testFindDevicesByTenantIdCustomerIdAndType() {
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Device title 1";
+        String type1 = "typeC";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            device = deviceService.saveDevice(device);
+            devicesType1.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeD";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            device = deviceService.saveDevice(device);
+            devicesType2.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        customerService.deleteCustomer(customerId);
+    }
+
 }
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
new file mode 100644
index 0000000..f7e47ce
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
@@ -0,0 +1,284 @@
+/**
+ * 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.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+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.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
+import org.thingsboard.server.dao.relation.EntityTypeFilter;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class RelationServiceImplTest extends AbstractServiceTest {
+
+    @Before
+    public void before() {
+    }
+
+    @After
+    public void after() {
+    }
+
+    @Test
+    public void testSaveRelation() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+
+        Assert.assertTrue(saveRelation(relation));
+
+        Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
+    }
+
+    @Test
+    public void testDeleteRelation() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+        AssetId subChildId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
+
+        saveRelation(relationA);
+        saveRelation(relationB);
+
+        Assert.assertTrue(relationService.deleteRelation(relationA).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+    }
+
+    @Test
+    public void testDeleteEntityRelations() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+        AssetId subChildId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
+
+        saveRelation(relationA);
+        saveRelation(relationB);
+
+        Assert.assertTrue(relationService.deleteEntityRelations(childId).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+    }
+
+    @Test
+    public void testFindFrom() throws ExecutionException, InterruptedException {
+        AssetId parentA = new AssetId(UUIDs.timeBased());
+        AssetId parentB = new AssetId(UUIDs.timeBased());
+        AssetId childA = new AssetId(UUIDs.timeBased());
+        AssetId childB = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE);
+
+        EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE);
+        EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE);
+
+        saveRelation(relationA1);
+        saveRelation(relationA2);
+
+        saveRelation(relationB1);
+        saveRelation(relationB2);
+
+        List<EntityRelation> relations = relationService.findByFrom(parentA, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(EntityRelation.CONTAINS_TYPE, relation.getType());
+            Assert.assertEquals(parentA, relation.getFrom());
+            Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
+        }
+
+        relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+
+        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFrom(parentB, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(EntityRelation.MANAGES_TYPE, relation.getType());
+            Assert.assertEquals(parentB, relation.getFrom());
+            Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
+        }
+
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+    }
+
+    private Boolean saveRelation(EntityRelation relationA1) throws ExecutionException, InterruptedException {
+        return relationService.saveRelation(relationA1).get();
+    }
+
+    @Test
+    public void testFindTo() throws ExecutionException, InterruptedException {
+        AssetId parentA = new AssetId(UUIDs.timeBased());
+        AssetId parentB = new AssetId(UUIDs.timeBased());
+        AssetId childA = new AssetId(UUIDs.timeBased());
+        AssetId childB = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE);
+
+        EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE);
+        EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE);
+
+        saveRelation(relationA1);
+        saveRelation(relationA2);
+
+        saveRelation(relationB1);
+        saveRelation(relationB2);
+
+        // Data propagation to views is async
+        Thread.sleep(3000);
+
+        List<EntityRelation> relations = relationService.findByTo(childA, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(childA, relation.getTo());
+            Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom()));
+        }
+
+        relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByTo(childB, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(childB, relation.getTo());
+            Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom()));
+        }
+    }
+
+    @Test
+    public void testCyclicRecursiveRelation() throws ExecutionException, InterruptedException {
+        // A -> B -> C -> A
+        AssetId assetA = new AssetId(UUIDs.timeBased());
+        AssetId assetB = new AssetId(UUIDs.timeBased());
+        AssetId assetC = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationC = new EntityRelation(assetC, assetA, EntityRelation.CONTAINS_TYPE);
+
+        saveRelation(relationA);
+        saveRelation(relationB);
+        saveRelation(relationC);
+
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1));
+        query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
+        List<EntityRelation> relations = relationService.findByQuery(query).get();
+        Assert.assertEquals(3, relations.size());
+        Assert.assertTrue(relations.contains(relationA));
+        Assert.assertTrue(relations.contains(relationB));
+        Assert.assertTrue(relations.contains(relationC));
+    }
+
+    @Test
+    public void testRecursiveRelation() throws ExecutionException, InterruptedException {
+        // A -> B -> [C,D]
+        AssetId assetA = new AssetId(UUIDs.timeBased());
+        AssetId assetB = new AssetId(UUIDs.timeBased());
+        AssetId assetC = new AssetId(UUIDs.timeBased());
+        DeviceId deviceD = new DeviceId(UUIDs.timeBased());
+
+        EntityRelation relationAB = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationBC = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationBD = new EntityRelation(assetB, deviceD, EntityRelation.CONTAINS_TYPE);
+
+
+        saveRelation(relationAB);
+        saveRelation(relationBC);
+        saveRelation(relationBD);
+
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1));
+        query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
+        List<EntityRelation> relations = relationService.findByQuery(query).get();
+        Assert.assertEquals(2, relations.size());
+        Assert.assertTrue(relations.contains(relationAB));
+        Assert.assertTrue(relations.contains(relationBC));
+    }
+
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException {
+        EntityRelation relation = new EntityRelation();
+        relation.setTo(new AssetId(UUIDs.timeBased()));
+        relation.setType(EntityRelation.CONTAINS_TYPE);
+        Assert.assertTrue(saveRelation(relation));
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException {
+        EntityRelation relation = new EntityRelation();
+        relation.setFrom(new AssetId(UUIDs.timeBased()));
+        relation.setType(EntityRelation.CONTAINS_TYPE);
+        Assert.assertTrue(saveRelation(relation));
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException {
+        EntityRelation relation = new EntityRelation();
+        relation.setFrom(new AssetId(UUIDs.timeBased()));
+        relation.setTo(new AssetId(UUIDs.timeBased()));
+        Assert.assertTrue(saveRelation(relation));
+    }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
index 376a88d..8a082fc 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
@@ -26,6 +26,7 @@ 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.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.relation.EntityRelationsQuery;
 import org.thingsboard.server.dao.relation.EntitySearchDirection;
@@ -55,13 +56,13 @@ public class RelationServiceTest extends AbstractServiceTest {
 
         Assert.assertTrue(saveRelation(relation));
 
-        Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE").get());
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE").get());
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
     }
 
     @Test
@@ -78,11 +79,11 @@ public class RelationServiceTest extends AbstractServiceTest {
 
         Assert.assertTrue(relationService.deleteRelation(relationA).get());
 
-        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
     }
 
     @Test
@@ -99,9 +100,9 @@ public class RelationServiceTest extends AbstractServiceTest {
 
         Assert.assertTrue(relationService.deleteEntityRelations(childId).get());
 
-        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
     }
 
     @Test
@@ -123,7 +124,7 @@ public class RelationServiceTest extends AbstractServiceTest {
         saveRelation(relationB1);
         saveRelation(relationB2);
 
-        List<EntityRelation> relations = relationService.findByFrom(parentA).get();
+        List<EntityRelation> relations = relationService.findByFrom(parentA, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(EntityRelation.CONTAINS_TYPE, relation.getType());
@@ -131,13 +132,13 @@ public class RelationServiceTest extends AbstractServiceTest {
             Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
         }
 
-        relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
 
-        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByFrom(parentB).get();
+        relations = relationService.findByFrom(parentB, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(EntityRelation.MANAGES_TYPE, relation.getType());
@@ -145,10 +146,10 @@ public class RelationServiceTest extends AbstractServiceTest {
             Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
         }
 
-        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
     }
 
@@ -178,26 +179,26 @@ public class RelationServiceTest extends AbstractServiceTest {
         // Data propagation to views is async
         Thread.sleep(3000);
 
-        List<EntityRelation> relations = relationService.findByTo(childA).get();
+        List<EntityRelation> relations = relationService.findByTo(childA, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(childA, relation.getTo());
             Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom()));
         }
 
-        relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(1, relations.size());
 
-        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(1, relations.size());
 
-        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByTo(childB).get();
+        relations = relationService.findByTo(childB, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(childB, relation.getTo());
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java
index 94b6f51..b26db0b 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java
@@ -71,6 +71,11 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest {
         assertEquals(alarm2Id, alarm.getId().getId());
     }
 
+    @Test
+    public void testFindAlarmByIdAsync() {
+        // TODO: implement
+    }
+
     private void saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) {
         Alarm alarm = new Alarm();
         alarm.setId(new AlarmId(id));
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
index ebee17e..4be102e 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
@@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListenableFuture;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -32,6 +33,7 @@ import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
 
 import static junit.framework.TestCase.assertFalse;
 import static org.junit.Assert.assertEquals;
@@ -56,7 +58,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
             UUID assetId = UUIDs.timeBased();
             UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2;
             UUID customerId = i % 2 == 0 ? customerId1 : customerId2;
-            saveAsset(assetId, tenantId, customerId, "ASSET_" + i);
+            saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1");
         }
         assertEquals(60, assetDao.find().size());
 
@@ -83,7 +85,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
             UUID assetId = UUIDs.timeBased();
             UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2;
             UUID customerId = i % 2 == 0 ? customerId1 : customerId2;
-            saveAsset(assetId, tenantId, customerId, "ASSET_" + i);
+            saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1");
         }
 
         TextPageLink pageLink1 = new TextPageLink(20, "ASSET_");
@@ -106,7 +108,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
         List<UUID> searchIds = new ArrayList<>();
         for (int i = 0; i < 30; i++) {
             UUID assetId = UUIDs.timeBased();
-            saveAsset(assetId, tenantId, customerId, "ASSET_" + i);
+            saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1");
             if (i % 3 == 0) {
                 searchIds.add(assetId);
             }
@@ -128,7 +130,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
         for (int i = 0; i < 30; i++) {
             UUID assetId = UUIDs.timeBased();
             UUID customerId = i%2 == 0 ? customerId1 : customerId2;
-            saveAsset(assetId, tenantId, customerId, "ASSET_" + i);
+            saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1");
             if (i % 3 == 0) {
                 searchIds.add(assetId);
             }
@@ -150,8 +152,8 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
         UUID customerId1 = UUIDs.timeBased();
         UUID customerId2 = UUIDs.timeBased();
         String name = "TEST_ASSET";
-        saveAsset(assetId1, tenantId1, customerId1, name);
-        saveAsset(assetId2, tenantId2, customerId2, name);
+        saveAsset(assetId1, tenantId1, customerId1, name, "TYPE_1");
+        saveAsset(assetId2, tenantId2, customerId2, name, "TYPE_1");
 
         Optional<Asset> assetOpt1 = assetDao.findAssetsByTenantIdAndName(tenantId2, name);
         assertTrue("Optional expected to be non-empty", assetOpt1.isPresent());
@@ -161,13 +163,61 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
         assertFalse("Optional expected to be empty", assetOpt2.isPresent());
     }
 
-    private void saveAsset(UUID id, UUID tenantId, UUID customerId, String name) {
+    @Test
+    public void testFindAssetsByTenantIdAndType() {
+        // TODO: implement
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdAndCustomerIdAndType() {
+        // TODO: implement
+    }
+
+    @Test
+    public void testFindTenantAssetTypesAsync() throws ExecutionException, InterruptedException {
+        UUID assetId1 = UUIDs.timeBased();
+        UUID assetId2 = UUIDs.timeBased();
+        UUID tenantId1 = UUIDs.timeBased();
+        UUID tenantId2 = UUIDs.timeBased();
+        UUID customerId1 = UUIDs.timeBased();
+        UUID customerId2 = UUIDs.timeBased();
+        saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_1", "TYPE_1");
+        saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_2", "TYPE_1");
+        saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_3", "TYPE_2");
+        saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_4", "TYPE_3");
+        saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_5", "TYPE_3");
+        saveAsset(UUIDs.timeBased(), tenantId1, customerId1, "TEST_ASSET_6", "TYPE_3");
+
+        saveAsset(UUIDs.timeBased(), tenantId2, customerId2, "TEST_ASSET_7", "TYPE_4");
+        saveAsset(UUIDs.timeBased(), tenantId2, customerId2, "TEST_ASSET_8", "TYPE_1");
+        saveAsset(UUIDs.timeBased(), tenantId2, customerId2, "TEST_ASSET_9", "TYPE_1");
+
+        ListenableFuture<List<TenantAssetType>> tenantAssetTypesFuture = assetDao.findTenantAssetTypesAsync();
+        List<TenantAssetType> tenantAssetTypes = tenantAssetTypesFuture.get();
+        assertNotNull(tenantAssetTypes);
+        List<TenantAssetType> tenant1Types = tenantAssetTypes.stream().filter(t -> t.getTenantId().getId().equals(tenantId1)).collect(Collectors.toList());
+        List<TenantAssetType> tenant2Types = tenantAssetTypes.stream().filter(t -> t.getTenantId().getId().equals(tenantId2)).collect(Collectors.toList());
+
+        assertEquals(3, tenant1Types.size());
+        assertTrue(tenant1Types.stream().anyMatch(t -> t.getType().equals("TYPE_1")));
+        assertTrue(tenant1Types.stream().anyMatch(t -> t.getType().equals("TYPE_2")));
+        assertTrue(tenant1Types.stream().anyMatch(t -> t.getType().equals("TYPE_3")));
+        assertFalse(tenant1Types.stream().anyMatch(t -> t.getType().equals("TYPE_4")));
+
+        assertEquals(2, tenant2Types.size());
+        assertTrue(tenant2Types.stream().anyMatch(t -> t.getType().equals("TYPE_1")));
+        assertTrue(tenant2Types.stream().anyMatch(t -> t.getType().equals("TYPE_4")));
+        assertFalse(tenant2Types.stream().anyMatch(t -> t.getType().equals("TYPE_2")));
+        assertFalse(tenant2Types.stream().anyMatch(t -> t.getType().equals("TYPE_3")));
+    }
+
+    private void saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type) {
         Asset asset = new Asset();
         asset.setId(new AssetId(id));
         asset.setTenantId(new TenantId(tenantId));
         asset.setCustomerId(new CustomerId(customerId));
         asset.setName(name);
-
+        asset.setType(type);
         assetDao.save(asset);
     }
 }

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 d19332b..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.2"
+  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.2"
-    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/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index 9ec8238..6c35bd2 100644
--- a/extensions/extension-kafka/pom.xml
+++ b/extensions/extension-kafka/pom.xml
@@ -22,7 +22,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index 93503e2..941165d 100644
--- a/extensions/extension-rabbitmq/pom.xml
+++ b/extensions/extension-rabbitmq/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml
index 9646e36..a732dc8 100644
--- a/extensions/extension-rest-api-call/pom.xml
+++ b/extensions/extension-rest-api-call/pom.xml
@@ -22,7 +22,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 63b34cf..2c1fdfe 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index b99d274..87ad867 100644
--- a/extensions-api/pom.xml
+++ b/extensions-api/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index ab5975f..923d474 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>

pom.xml 4(+2 -2)

diff --git a/pom.xml b/pom.xml
index 0d813f0..3bdacd3 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.thingsboard</groupId>
     <artifactId>thingsboard</artifactId>
-    <version>1.2.3-SNAPSHOT</version>
+    <version>1.3.0-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <name>Thingsboard</name>
@@ -46,7 +46,7 @@
         <guava.version>18.0</guava.version>
         <commons-lang3.version>3.4</commons-lang3.version>
         <commons-validator.version>1.5.0</commons-validator.version>
-        <jackson.version>2.7.3</jackson.version>
+        <jackson.version>2.8.8.1</jackson.version>
         <json-schema-validator.version>2.2.6</json-schema-validator.version>
         <scala.version>2.11</scala.version>
         <akka.version>2.4.2</akka.version>

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

diff --git a/tools/pom.xml b/tools/pom.xml
index 96235e2..2b83e13 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index ef3953d..8f15edc 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 16e5626..004c57e 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 15c640c..2de70a7 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/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/transport/pom.xml b/transport/pom.xml
index bca6091..fb47647 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>

ui/package.json 2(+1 -1)

diff --git a/ui/package.json b/ui/package.json
index ddcc96e..73d7bcd 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
 {
   "name": "thingsboard",
   "private": true,
-  "version": "1.2.3",
+  "version": "1.3.0",
   "description": "Thingsboard UI",
   "licenses": [
     {

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

diff --git a/ui/pom.xml b/ui/pom.xml
index 1ffaa67..1efb29a 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.2.3-SNAPSHOT</version>
+        <version>1.3.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js
index f685b1e..2fbb11d 100644
--- a/ui/src/app/api/asset.service.js
+++ b/ui/src/app/api/asset.service.js
@@ -31,7 +31,8 @@ function AssetService($http, $q, customerService, userService) {
         getTenantAssets: getTenantAssets,
         getCustomerAssets: getCustomerAssets,
         findByQuery: findByQuery,
-        fetchAssetsByNameFilter: fetchAssetsByNameFilter
+        fetchAssetsByNameFilter: fetchAssetsByNameFilter,
+        getAssetTypes: getAssetTypes
     }
 
     return service;
@@ -152,7 +153,7 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
-    function getTenantAssets(pageLink, applyCustomersInfo, config) {
+    function getTenantAssets(pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/tenant/assets?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -164,6 +165,9 @@ function AssetService($http, $q, customerService, userService) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomersInfo(response.data.data).then(
@@ -184,7 +188,7 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
-    function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config) {
+    function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/assets?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -196,6 +200,9 @@ function AssetService($http, $q, customerService, userService) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
@@ -258,4 +265,15 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
+    function getAssetTypes() {
+        var deferred = $q.defer();
+        var url = '/api/asset/types';
+        $http.get(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
 }
diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js
index 35c14fb..53d5341 100644
--- a/ui/src/app/api/attribute.service.js
+++ b/ui/src/app/api/attribute.service.js
@@ -25,6 +25,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) 
     var service = {
         getEntityKeys: getEntityKeys,
         getEntityTimeseriesValues: getEntityTimeseriesValues,
+        getEntityAttributesValues: getEntityAttributesValues,
         getEntityAttributes: getEntityAttributes,
         subscribeForEntityAttributes: subscribeForEntityAttributes,
         unsubscribeForEntityAttributes: unsubscribeForEntityAttributes,
@@ -81,6 +82,20 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) 
         return deferred.promise;
     }
 
+    function getEntityAttributesValues(entityType, entityId, attributeScope, keys, config) {
+        var deferred = $q.defer();
+        var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/values/attributes/' + attributeScope;
+        if (keys && keys.length) {
+            url += '?keys=' + keys;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
     function processAttributes(attributes, query, deferred, successCallback, update, apply) {
         attributes = $filter('orderBy')(attributes, query.order);
         if (query.search != null) {
@@ -200,15 +215,48 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) 
     function saveEntityAttributes(entityType, entityId, attributeScope, attributes) {
         var deferred = $q.defer();
         var attributesData = {};
+        var deleteAttributes = [];
         for (var a=0; a<attributes.length;a++) {
-            attributesData[attributes[a].key] = attributes[a].value;
+            if (angular.isDefined(attributes[a].value) && attributes[a].value !== null) {
+                attributesData[attributes[a].key] = attributes[a].value;
+            } else {
+                deleteAttributes.push(attributes[a]);
+            }
+        }
+        var deleteEntityAttributesPromise;
+        if (deleteAttributes.length) {
+            deleteEntityAttributesPromise = deleteEntityAttributes(entityType, entityId, attributeScope, deleteAttributes);
+        }
+        if (Object.keys(attributesData).length) {
+            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope;
+            $http.post(url, attributesData).then(function success(response) {
+                if (deleteEntityAttributesPromise) {
+                    deleteEntityAttributesPromise.then(
+                        function success() {
+                            deferred.resolve(response.data);
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    )
+                } else {
+                    deferred.resolve(response.data);
+                }
+            }, function fail() {
+                deferred.reject();
+            });
+        } else if (deleteEntityAttributesPromise) {
+            deleteEntityAttributesPromise.then(
+                function success() {
+                    deferred.resolve();
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            )
+        } else {
+            deferred.resolve();
         }
-        var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope;
-        $http.post(url, attributesData).then(function success(response) {
-            deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
-        });
         return deferred.promise;
     }
 
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
index b2a7897..743fbe9 100644
--- a/ui/src/app/api/dashboard.service.js
+++ b/ui/src/app/api/dashboard.service.js
@@ -24,6 +24,8 @@ function DashboardService($http, $q, $location, customerService) {
         getCustomerDashboards: getCustomerDashboards,
         getServerTimeDiff: getServerTimeDiff,
         getDashboard: getDashboard,
+        getDashboardInfo: getDashboardInfo,
+        getTenantDashboardsByTenantId: getTenantDashboardsByTenantId,
         getTenantDashboards: getTenantDashboards,
         deleteDashboard: deleteDashboard,
         saveDashboard: saveDashboard,
@@ -34,6 +36,26 @@ function DashboardService($http, $q, $location, customerService) {
 
     return service;
 
+    function getTenantDashboardsByTenantId(tenantId, pageLink) {
+        var deferred = $q.defer();
+        var url = '/api/tenant/' + tenantId + '/dashboards?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
     function getTenantDashboards(pageLink) {
         var deferred = $q.defer();
         var url = '/api/tenant/dashboards?limit=' + pageLink.limit;
@@ -94,7 +116,7 @@ function DashboardService($http, $q, $location, customerService) {
         var deferred = $q.defer();
         var url = '/api/dashboard/serverTime';
         var ct1 = Date.now();
-        $http.get(url, null).then(function success(response) {
+        $http.get(url, { ignoreLoading: true }).then(function success(response) {
             var ct2 = Date.now();
             var st = response.data;
             var stDiff = Math.ceil(st - (ct1+ct2)/2);
@@ -116,6 +138,17 @@ function DashboardService($http, $q, $location, customerService) {
         return deferred.promise;
     }
 
+    function getDashboardInfo(dashboardId) {
+        var deferred = $q.defer();
+        var url = '/api/dashboard/info/' + dashboardId;
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
     function saveDashboard(dashboard) {
         var deferred = $q.defer();
         var url = '/api/dashboard';
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index 6f15363..b1d1bb6 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -40,12 +40,14 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         saveDeviceAttributes: saveDeviceAttributes,
         deleteDeviceAttributes: deleteDeviceAttributes,
         sendOneWayRpcCommand: sendOneWayRpcCommand,
-        sendTwoWayRpcCommand: sendTwoWayRpcCommand
+        sendTwoWayRpcCommand: sendTwoWayRpcCommand,
+        findByQuery: findByQuery,
+        getDeviceTypes: getDeviceTypes
     }
 
     return service;
 
-    function getTenantDevices(pageLink, applyCustomersInfo, config) {
+    function getTenantDevices(pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/tenant/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -57,6 +59,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomersInfo(response.data.data).then(
@@ -77,7 +82,7 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         return deferred.promise;
     }
 
-    function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
+    function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -89,6 +94,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
@@ -270,4 +278,30 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         return deferred.promise;
     }
 
+    function findByQuery(query, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/devices';
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.post(url, query, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getDeviceTypes() {
+        var deferred = $q.defer();
+        var url = '/api/device/types';
+        $http.get(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
 }
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index 4904a6a..891f9d8 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -20,18 +20,23 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
     .name;
 
 /*@ngInject*/
-function EntityService($http, $q, userService, deviceService,
+function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
                        assetService, tenantService, customerService,
-                       ruleService, pluginService, 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,
-        createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
+        createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
+        getRelatedEntities: getRelatedEntities,
+        saveRelatedEntity: saveRelatedEntity,
+        getRelatedEntity: getRelatedEntity,
+        deleteRelatedEntity: deleteRelatedEntity,
+        moveEntity: moveEntity,
+        copyEntity: copyEntity
     };
 
     return service;
@@ -57,6 +62,15 @@ function EntityService($http, $q, userService, deviceService,
             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;
     }
@@ -64,14 +78,18 @@ function EntityService($http, $q, userService, deviceService,
     function getEntity(entityType, entityId, config) {
         var deferred = $q.defer();
         var promise = getEntityPromise(entityType, entityId, 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;
     }
 
@@ -124,6 +142,15 @@ function EntityService($http, $q, userService, deviceService,
             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;
     }
@@ -131,34 +158,38 @@ function EntityService($http, $q, userService, deviceService,
     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:
@@ -173,48 +204,48 @@ function EntityService($http, $q, userService, deviceService,
             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) {
@@ -474,4 +505,321 @@ function EntityService($http, $q, userService, deviceService,
         }
     }
 
+    function getRelatedEntities(rootEntityId, entityType, entitySubTypes, maxLevel, keys, typeTranslatePrefix) {
+        var deferred = $q.defer();
+
+        var entitySearchQuery = constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel);
+        if (!entitySearchQuery) {
+            deferred.reject();
+        } else {
+            var findByQueryPromise;
+            if (entityType == types.entityType.asset) {
+                findByQueryPromise = assetService.findByQuery(entitySearchQuery, true, {ignoreLoading: true});
+            } else if (entityType == types.entityType.device) {
+                findByQueryPromise = deviceService.findByQuery(entitySearchQuery, true, {ignoreLoading: true});
+            }
+            findByQueryPromise.then(
+                function success(entities) {
+                    var entitiesTasks = [];
+                    for (var i=0;i<entities.length;i++) {
+                        var entity = entities[i];
+                        var entityPromise = constructEntity(entity, keys, typeTranslatePrefix);
+                        entitiesTasks.push(entityPromise);
+                    }
+                    $q.all(entitiesTasks).then(
+                        function success(entities) {
+                            deferred.resolve(entities);
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
+    function saveRelatedEntity(relatedEntity, parentEntityId, keys) {
+        var deferred = $q.defer();
+        if (relatedEntity.id.id) {
+            updateRelatedEntity(relatedEntity, keys, deferred);
+        } else {
+            addRelatedEntity(relatedEntity, parentEntityId, keys, deferred);
+        }
+        return deferred.promise;
+    }
+
+    function getRelatedEntity(entityId, keys, typeTranslatePrefix) {
+        var deferred = $q.defer();
+        getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then(
+            function success(entity) {
+                constructEntity(entity, keys, typeTranslatePrefix).then(
+                    function success(relatedEntity) {
+                        deferred.resolve(relatedEntity);
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function deleteEntityPromise(entityId) {
+        if (entityId.entityType == types.entityType.asset) {
+            return assetService.deleteAsset(entityId.id);
+        } else if (entityId.entityType == types.entityType.device) {
+            return deviceService.deleteDevice(entityId.id);
+        }
+    }
+
+    function deleteRelatedEntity(entityId, deleteRelatedEntityTypes) {
+        var deferred = $q.defer();
+        if (deleteRelatedEntityTypes) {
+            var deleteRelatedEntitiesTasks = [];
+            entityRelationService.findByFrom(entityId.id, entityId.entityType).then(
+                function success(entityRelations) {
+                    for (var i=0;i<entityRelations.length;i++) {
+                        var entityRelation = entityRelations[i];
+                        var relationEntityId = entityRelation.to;
+                        if (deleteRelatedEntityTypes.length == 0 || deleteRelatedEntityTypes.indexOf(relationEntityId.entityType) > -1) {
+                            var deleteRelatedEntityPromise = deleteRelatedEntity(relationEntityId, deleteRelatedEntityTypes);
+                            deleteRelatedEntitiesTasks.push(deleteRelatedEntityPromise);
+                        }
+                    }
+                    deleteRelatedEntitiesTasks.push(deleteEntityPromise(entityId));
+                    $q.all(deleteRelatedEntitiesTasks).then(
+                        function success() {
+                            deferred.resolve();
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            )
+        } else {
+            deleteEntityPromise(entityId).then(
+                function success() {
+                    deferred.resolve();
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
+    function moveEntity(entityId, prevParentId, targetParentId) {
+        var deferred = $q.defer();
+        entityRelationService.deleteRelation(prevParentId.id, prevParentId.entityType,
+            types.entityRelationType.contains, entityId.id, entityId.entityType).then(
+            function success() {
+                var relation = {
+                    from: targetParentId,
+                    to: entityId,
+                    type: types.entityRelationType.contains
+                };
+                entityRelationService.saveRelation(relation).then(
+                    function success() {
+                        deferred.resolve();
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function copyEntity(entity, targetParentId, keys) {
+        var deferred = $q.defer();
+        if (!entity.id && !entity.id.id) {
+            deferred.reject();
+        } else {
+            getRelatedEntity(entity.id, keys).then(
+                function success(relatedEntity) {
+                    delete relatedEntity.id.id;
+                    relatedEntity.name = entity.name;
+                    saveRelatedEntity(relatedEntity, targetParentId, keys).then(
+                        function success(savedEntity) {
+                            deferred.resolve(savedEntity);
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
+    function saveEntityPromise(entity) {
+        var entityType = entity.id.entityType;
+        if (!entity.id.id) {
+            delete entity.id;
+        }
+        if (entityType == types.entityType.asset) {
+            return assetService.saveAsset(entity);
+        } else if (entityType == types.entityType.device) {
+            return deviceService.saveDevice(entity);
+        }
+    }
+
+    function addRelatedEntity(relatedEntity, parentEntityId, keys, deferred) {
+        var entity = {};
+        entity.id = relatedEntity.id;
+        entity.name = relatedEntity.name;
+        entity.type = relatedEntity.type;
+        saveEntityPromise(entity).then(
+            function success(entity) {
+                relatedEntity.id = entity.id;
+                var relation = {
+                    from: parentEntityId,
+                    to: relatedEntity.id,
+                    type: types.entityRelationType.contains
+                };
+                entityRelationService.saveRelation(relation).then(
+                    function success() {
+                        updateEntity(entity, relatedEntity, keys, deferred);
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+    }
+
+    function updateRelatedEntity(relatedEntity, keys, deferred) {
+        getEntityPromise(relatedEntity.id.entityType, relatedEntity.id.id, {ignoreLoading: true}).then(
+            function success(entity) {
+                updateEntity(entity, relatedEntity, keys, deferred);
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+    }
+
+    function updateEntity(entity, relatedEntity, keys, deferred) {
+        if (!angular.equals(entity.name, relatedEntity.name) || !angular.equals(entity.type, relatedEntity.type)) {
+            entity.name = relatedEntity.name;
+            entity.type = relatedEntity.type;
+            saveEntityPromise(entity).then(
+                function success (entity) {
+                    updateEntityAttributes(entity, relatedEntity, keys, deferred);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        } else {
+            updateEntityAttributes(entity, relatedEntity, keys, deferred);
+        }
+    }
+
+    function updateEntityAttributes(entity, relatedEntity, keys, deferred) {
+        var attributes = [];
+        for (var i = 0; i < keys.length; i++) {
+            var key = keys[i];
+            attributes.push({key: key, value: relatedEntity[key]});
+        }
+        attributeService.saveEntityAttributes(entity.id.entityType, entity.id.id, types.attributesScope.server.value, attributes)
+            .then(
+                function success() {
+                    deferred.resolve(relatedEntity);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+    }
+
+    function constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel) {
+
+        var searchQuery = {
+            parameters: {
+                rootId: rootEntityId.id,
+                rootType: rootEntityId.entityType,
+                direction: types.entitySearchDirection.from
+            },
+            relationType: types.entityRelationType.contains
+        };
+
+        if (maxLevel) {
+            searchQuery.parameters.maxLevel = maxLevel;
+        } else {
+            searchQuery.parameters.maxLevel = 1;
+        }
+
+        if (entityType == types.entityType.asset) {
+            searchQuery.assetTypes = entitySubTypes;
+        } else if (entityType == types.entityType.device) {
+            searchQuery.deviceTypes = entitySubTypes;
+        } else {
+            return null; //Not supported
+        }
+
+        return searchQuery;
+    }
+
+    function constructEntity(entity, keys, typeTranslatePrefix) {
+        var deferred = $q.defer();
+        if (typeTranslatePrefix) {
+            entity.typeName = $translate.instant(typeTranslatePrefix+'.'+entity.type);
+        } else {
+            entity.typeName = entity.type;
+        }
+        attributeService.getEntityAttributesValues(entity.id.entityType, entity.id.id,
+            types.attributesScope.server.value, keys.join(','),
+            {ignoreLoading: true}).then(
+            function success(attributes) {
+                if (attributes && attributes.length > 0) {
+                    for (var i=0;i<keys.length;i++) {
+                        var key = keys[i];
+                        entity[key] = getAttributeValue(attributes, key);
+                    }
+                }
+                deferred.resolve(entity);
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function getAttributeValue(attributes, key) {
+        var foundAttributes = $filter('filter')(attributes, {key: key}, true);
+        if (foundAttributes.length > 0) {
+            return foundAttributes[0].value;
+        } else {
+            return null;
+        }
+    }
+
 }
\ No newline at end of file
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/api/subscription.js b/ui/src/app/api/subscription.js
index 633bcc3..fb774a1 100644
--- a/ui/src/app/api/subscription.js
+++ b/ui/src/app/api/subscription.js
@@ -126,6 +126,7 @@ export default class Subscription {
                         dataKey: dataKey,
                         dataIndex: dataIndex++
                     };
+                    legendKey.dataKey.hidden = false;
                     this.legendData.keys.push(legendKey);
                     var legendKeyData = {
                         min: null,
@@ -146,11 +147,11 @@ export default class Subscription {
             this.legendData.keys = this.ctx.$filter('orderBy')(this.legendData.keys, '+label');
             registration = this.ctx.$scope.$watch(
                 function() {
-                    return subscription.legendData.data;
+                    return subscription.legendData.keys;
                 },
                 function (newValue, oldValue) {
                     for(var i = 0; i < newValue.length; i++) {
-                        if(newValue[i].hidden != oldValue[i].hidden) {
+                        if(newValue[i].dataKey.hidden != oldValue[i].dataKey.hidden) {
                             subscription.updateDataVisibility(i);
                         }
                     }
@@ -307,7 +308,7 @@ export default class Subscription {
     }
 
     updateDataVisibility(index) {
-        var hidden = this.legendData.data[index].hidden;
+        var hidden = this.legendData.keys[index].dataKey.hidden;
         if (hidden) {
             this.hiddenData[index].data = this.data[index].data;
             this.data[index].data = [];
@@ -418,7 +419,7 @@ export default class Subscription {
         this.notifyDataLoaded();
         var update = true;
         var currentData;
-        if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) {
+        if (this.displayLegend && this.legendData.keys[datasourceIndex + dataKeyIndex].dataKey.hidden) {
             currentData = this.hiddenData[datasourceIndex + dataKeyIndex];
         } else {
             currentData = this.data[datasourceIndex + dataKeyIndex];
@@ -445,18 +446,21 @@ export default class Subscription {
     }
 
     updateLegend(dataIndex, data, apply) {
+        var dataKey = this.legendData.keys[dataIndex].dataKey;
+        var decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals;
+        var units = dataKey.units && dataKey.units.length ? dataKey.units : this.units;
         var legendKeyData = this.legendData.data[dataIndex];
         if (this.legendConfig.showMin) {
-            legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units);
+            legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), decimals, units);
         }
         if (this.legendConfig.showMax) {
-            legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units);
+            legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), decimals, units);
         }
         if (this.legendConfig.showAvg) {
-            legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units);
+            legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), decimals, units);
         }
         if (this.legendConfig.showTotal) {
-            legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units);
+            legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), decimals, units);
         }
         this.callbacks.legendDataUpdated(this, apply !== false);
     }
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 4dc41f7..a5bb36c 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -262,7 +262,13 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
 
         function fetchAllowedDashboardIds() {
             var pageLink = {limit: 100};
-            dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
+            var fetchDashboardsPromise;
+            if (currentUser.authority === 'TENANT_ADMIN') {
+                fetchDashboardsPromise = dashboardService.getTenantDashboards(pageLink);
+            } else {
+                fetchDashboardsPromise = dashboardService.getCustomerDashboards(currentUser.customerId, pageLink);
+            }
+            fetchDashboardsPromise.then(
                 function success(result) {
                     var dashboards = result.data;
                     for (var d=0;d<dashboards.length;d++) {
@@ -296,7 +302,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
                             if (userForceFullscreen()) {
                                 $rootScope.forceFullscreen = true;
                             }
-                            if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
+                            if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' ||
+                                currentUser.authority === 'CUSTOMER_USER')) {
                                 fetchAllowedDashboardIds();
                             } else {
                                 deferred.resolve();
@@ -436,7 +443,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
 
     function forceDefaultPlace(to, params) {
         if (currentUser && isAuthenticated()) {
-            if (currentUser.authority === 'CUSTOMER_USER') {
+            if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {
                 if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) {
                     if (to.name === 'home.profile') {
                         if (userHasProfile()) {
@@ -458,7 +465,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
     function gotoDefaultPlace(params) {
         if (currentUser && isAuthenticated()) {
             var place = 'home.links';
-            if (currentUser.authority === 'CUSTOMER_USER') {
+            if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {
                 if (userHasDefaultDashboard()) {
                     place = 'home.dashboards.dashboard';
                     params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
index 7f7e7a5..074437d 100644
--- a/ui/src/app/app.config.js
+++ b/ui/src/app/app.config.js
@@ -49,7 +49,7 @@ export default function AppConfig($provide,
     $translateProvider.useSanitizeValueStrategy('sce');
     $translateProvider.preferredLanguage('en_US');
     $translateProvider.useLocalStorage();
-    $translateProvider.useMissingTranslationHandlerLog();
+    $translateProvider.useMissingTranslationHandler('tbMissingTranslationHandler');
     $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
 
     addLocaleKorean(locales);
@@ -160,10 +160,6 @@ export default function AppConfig($provide,
             indigoTheme();
         }
 
-        $mdThemingProvider.theme('tb-search-input', 'default')
-            .primaryPalette('tb-primary')
-            .backgroundPalette('tb-primary');
-
         $mdThemingProvider.setDefaultTheme('default');
         //$mdThemingProvider.alwaysWatchTheme(true);
     }
diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js
index d0944ae..1253891 100644
--- a/ui/src/app/asset/asset.controller.js
+++ b/ui/src/app/asset/asset.controller.js
@@ -47,7 +47,8 @@ export function AssetCardController(types) {
 
 
 /*@ngInject*/
-export function AssetController(userService, assetService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams,
+                                $document, $mdDialog, $q, $translate, types) {
 
     var customerId = $stateParams.customerId;
 
@@ -129,8 +130,8 @@ export function AssetController(userService, assetService, customerService, $sta
         }
 
         if (vm.assetsScope === 'tenant') {
-            fetchAssetsFunction = function (pageLink) {
-                return assetService.getTenantAssets(pageLink, true);
+            fetchAssetsFunction = function (pageLink, assetType) {
+                return assetService.getTenantAssets(pageLink, true, null, assetType);
             };
             deleteAssetFunction = function (assetId) {
                 return assetService.deleteAsset(assetId);
@@ -229,8 +230,8 @@ export function AssetController(userService, assetService, customerService, $sta
 
 
         } else if (vm.assetsScope === 'customer' || vm.assetsScope === 'customer_user') {
-            fetchAssetsFunction = function (pageLink) {
-                return assetService.getCustomerAssets(customerId, pageLink, true);
+            fetchAssetsFunction = function (pageLink, assetType) {
+                return assetService.getCustomerAssets(customerId, pageLink, true, null, assetType);
             };
             deleteAssetFunction = function (assetId) {
                 return assetService.unassignAssetFromCustomer(assetId);
@@ -333,6 +334,7 @@ export function AssetController(userService, assetService, customerService, $sta
         var deferred = $q.defer();
         assetService.saveAsset(asset).then(
             function success(savedAsset) {
+                $rootScope.$broadcast('assetSaved');
                 var assets = [ savedAsset ];
                 customerService.applyAssignedCustomersInfo(assets).then(
                     function success(items) {
diff --git a/ui/src/app/asset/asset.directive.js b/ui/src/app/asset/asset.directive.js
index 8c13082..7110e6a 100644
--- a/ui/src/app/asset/asset.directive.js
+++ b/ui/src/app/asset/asset.directive.js
@@ -25,6 +25,7 @@ export default function AssetDirective($compile, $templateCache, toast, $transla
         var template = $templateCache.get(assetFieldsetTemplate);
         element.html(template);
 
+        scope.types = types;
         scope.isAssignedToCustomer = false;
         scope.isPublic = false;
         scope.assignedCustomer = null;
diff --git a/ui/src/app/asset/asset.routes.js b/ui/src/app/asset/asset.routes.js
index c9a312d..732f74c 100644
--- a/ui/src/app/asset/asset.routes.js
+++ b/ui/src/app/asset/asset.routes.js
@@ -20,7 +20,7 @@ import assetsTemplate from './assets.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function AssetRoutes($stateProvider) {
+export default function AssetRoutes($stateProvider, types) {
     $stateProvider
         .state('home.assets', {
             url: '/assets',
@@ -37,6 +37,8 @@ export default function AssetRoutes($stateProvider) {
             data: {
                 assetsType: 'tenant',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.asset,
                 pageTitle: 'asset.assets'
             },
             ncyBreadcrumb: {
@@ -58,6 +60,8 @@ export default function AssetRoutes($stateProvider) {
             data: {
                 assetsType: 'customer',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.asset,
                 pageTitle: 'customer.assets'
             },
             ncyBreadcrumb: {
diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html
index 3c06558..30d0483 100644
--- a/ui/src/app/asset/asset-card.tpl.html
+++ b/ui/src/app/asset/asset-card.tpl.html
@@ -15,5 +15,8 @@
     limitations under the License.
 
 -->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
-<div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
+<div flex layout="column" style="margin-top: -10px;">
+    <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+    <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+    <div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
+</div>
diff --git a/ui/src/app/asset/asset-fieldset.tpl.html b/ui/src/app/asset/asset-fieldset.tpl.html
index 8cf0c96..d921b2e 100644
--- a/ui/src/app/asset/asset-fieldset.tpl.html
+++ b/ui/src/app/asset/asset-fieldset.tpl.html
@@ -56,13 +56,13 @@
                 <div translate ng-message="required">asset.name-required</div>
             </div>
         </md-input-container>
-        <md-input-container class="md-block">
-            <label translate>asset.type</label>
-            <input required name="type" ng-model="asset.type">
-            <div ng-messages="theForm.name.$error">
-                <div translate ng-message="required">asset.type-required</div>
-            </div>
-        </md-input-container>
+        <tb-entity-subtype-autocomplete
+                ng-disabled="loading || !isEdit"
+                tb-required="true"
+                the-form="theForm"
+                ng-model="asset.type"
+                entity-type="types.entityType.asset">
+        </tb-entity-subtype-autocomplete>
         <md-input-container class="md-block">
             <label translate>asset.description</label>
             <textarea ng-model="asset.additionalInfo.description" rows="2"></textarea>
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/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js
index 0bc73d1..78136df 100644
--- a/ui/src/app/common/dashboard-utils.service.js
+++ b/ui/src/app/common/dashboard-utils.service.js
@@ -19,10 +19,22 @@ export default angular.module('thingsboard.dashboardUtils', [])
     .name;
 
 /*@ngInject*/
-function DashboardUtils(types, timeService) {
+function DashboardUtils(types, utils, timeService) {
 
     var service = {
-        validateAndUpdateDashboard: validateAndUpdateDashboard
+        validateAndUpdateDashboard: validateAndUpdateDashboard,
+        getRootStateId: getRootStateId,
+        createSingleWidgetDashboard: createSingleWidgetDashboard,
+        getStateLayoutsData: getStateLayoutsData,
+        createDefaultState: createDefaultState,
+        createDefaultLayoutData: createDefaultLayoutData,
+        setLayouts: setLayouts,
+        updateLayoutSettings: updateLayoutSettings,
+        addWidgetToLayout: addWidgetToLayout,
+        removeWidgetFromLayout: removeWidgetFromLayout,
+        isSingleLayoutDashboard: isSingleLayoutDashboard,
+        removeUnusedWidgets: removeUnusedWidgets,
+        getWidgetsArray: getWidgetsArray
     };
 
     return service;
@@ -69,39 +81,358 @@ function DashboardUtils(types, timeService) {
             widget.config.datasources = [];
         }
         widget.config.datasources.forEach(function(datasource) {
-             if (datasource.type === 'device') {
-                 datasource.type = types.datasourceType.entity;
-             }
-             if (datasource.deviceAliasId) {
-                 datasource.entityAliasId = datasource.deviceAliasId;
-                 delete datasource.deviceAliasId;
-             }
+            if (datasource.type === 'device') {
+                datasource.type = types.datasourceType.entity;
+            }
+            if (datasource.deviceAliasId) {
+                datasource.entityAliasId = datasource.deviceAliasId;
+                delete datasource.deviceAliasId;
+            }
         });
+        return widget;
+    }
+
+    function createDefaultLayoutData() {
+        return {
+            widgets: {},
+            gridSettings: {
+                backgroundColor: '#eeeeee',
+                color: 'rgba(0,0,0,0.870588)',
+                columns: 24,
+                margins: [10, 10],
+                backgroundSizeMode: '100%'
+            }
+        };
+    }
+
+    function createDefaultLayouts() {
+        return {
+            'main': createDefaultLayoutData()
+        };
+    }
+
+    function createDefaultState(name, root) {
+        return {
+            name: name,
+            root: root,
+            layouts: createDefaultLayouts()
+        }
     }
 
     function validateAndUpdateDashboard(dashboard) {
         if (!dashboard.configuration) {
-            dashboard.configuration = {
-                widgets: [],
-                entityAliases: {}
-            };
+            dashboard.configuration = {};
         }
         if (angular.isUndefined(dashboard.configuration.widgets)) {
-            dashboard.configuration.widgets = [];
+            dashboard.configuration.widgets = {};
+        } else if (angular.isArray(dashboard.configuration.widgets)) {
+            var widgetsMap = {};
+            dashboard.configuration.widgets.forEach(function (widget) {
+                if (!widget.id) {
+                    widget.id = utils.guid();
+                }
+                widgetsMap[widget.id] = validateAndUpdateWidget(widget);
+            });
+            dashboard.configuration.widgets = widgetsMap;
         }
-        dashboard.configuration.widgets.forEach(function(widget) {
-            validateAndUpdateWidget(widget);
-        });
+        if (angular.isUndefined(dashboard.configuration.states)) {
+            dashboard.configuration.states = {
+                'default': createDefaultState('Default', true)
+            };
+
+            var mainLayout = dashboard.configuration.states['default'].layouts['main'];
+            for (var id in dashboard.configuration.widgets) {
+                var widget = dashboard.configuration.widgets[id];
+                mainLayout.widgets[id] = {
+                    sizeX: widget.sizeX,
+                    sizeY: widget.sizeY,
+                    row: widget.row,
+                    col: widget.col,
+                };
+            }
+        } else {
+            var states = dashboard.configuration.states;
+            var rootFound = false;
+            for (var stateId in states) {
+                var state = states[stateId];
+                if (angular.isUndefined(state.root)) {
+                    state.root = false;
+                } else if (state.root) {
+                    rootFound = true;
+                }
+            }
+            if (!rootFound) {
+                var firstStateId = Object.keys(states)[0];
+                states[firstStateId].root = true;
+            }
+        }
+        dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration);
+
         if (angular.isUndefined(dashboard.configuration.timewindow)) {
             dashboard.configuration.timewindow = timeService.defaultTimewindow();
         }
+        if (angular.isUndefined(dashboard.configuration.settings)) {
+            dashboard.configuration.settings = {};
+            dashboard.configuration.settings.stateControllerId = 'default';
+            dashboard.configuration.settings.showTitle = true;
+            dashboard.configuration.settings.showDashboardsSelect = true;
+            dashboard.configuration.settings.showEntitiesSelect = true;
+            dashboard.configuration.settings.showDashboardTimewindow = true;
+            dashboard.configuration.settings.showDashboardExport = true;
+            dashboard.configuration.settings.toolbarAlwaysOpen = false;
+        } else {
+            if (angular.isUndefined(dashboard.configuration.settings.stateControllerId)) {
+                dashboard.configuration.settings.stateControllerId = 'default';
+            }
+        }
         if (angular.isDefined(dashboard.configuration.gridSettings)) {
-            if (angular.isDefined(dashboard.configuration.gridSettings.showDevicesSelect)) {
-                dashboard.configuration.gridSettings.showEntitiesSelect = dashboard.configuration.gridSettings.showDevicesSelect;
-                delete dashboard.configuration.gridSettings.showDevicesSelect;
+            var gridSettings = dashboard.configuration.gridSettings;
+            if (angular.isDefined(gridSettings.showTitle)) {
+                dashboard.configuration.settings.showTitle = gridSettings.showTitle;
+                delete gridSettings.showTitle;
+            }
+            if (angular.isDefined(gridSettings.titleColor)) {
+                dashboard.configuration.settings.titleColor = gridSettings.titleColor;
+                delete gridSettings.titleColor;
+            }
+            if (angular.isDefined(gridSettings.showDevicesSelect)) {
+                dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect;
+                delete gridSettings.showDevicesSelect;
+            }
+            if (angular.isDefined(gridSettings.showEntitiesSelect)) {
+                dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect;
+                delete gridSettings.showEntitiesSelect;
+            }
+            if (angular.isDefined(gridSettings.showDashboardTimewindow)) {
+                dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow;
+                delete gridSettings.showDashboardTimewindow;
+            }
+            if (angular.isDefined(gridSettings.showDashboardExport)) {
+                dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport;
+                delete gridSettings.showDashboardExport;
             }
+            dashboard.configuration.states['default'].layouts['main'].gridSettings = gridSettings;
+            delete dashboard.configuration.gridSettings;
         }
-        dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration);
         return dashboard;
     }
+
+    function getRootStateId(states) {
+        for (var stateId in states) {
+            var state = states[stateId];
+            if (state.root) {
+                return stateId;
+            }
+        }
+        return Object.keys(states)[0];
+    }
+
+    function createSingleWidgetDashboard(widget) {
+        if (!widget.id) {
+            widget.id = utils.guid();
+        }
+        var dashboard = {};
+        dashboard = validateAndUpdateDashboard(dashboard);
+        dashboard.configuration.widgets[widget.id] = widget;
+        dashboard.configuration.states['default'].layouts['main'].widgets[widget.id] = {
+            sizeX: widget.sizeX,
+            sizeY: widget.sizeY,
+            row: widget.row,
+            col: widget.col,
+        };
+        return dashboard;
+    }
+
+    function getStateLayoutsData(dashboard, targetState) {
+        var dashboardConfiguration = dashboard.configuration;
+        var states = dashboardConfiguration.states;
+        var state = states[targetState];
+        if (state) {
+            var allWidgets = dashboardConfiguration.widgets;
+            var result = {};
+            for (var l in state.layouts) {
+                var layout = state.layouts[l];
+                if (layout) {
+                    result[l] = {
+                        widgets: [],
+                        widgetLayouts: {},
+                        gridSettings: {}
+                    }
+                    for (var id in layout.widgets) {
+                        result[l].widgets.push(allWidgets[id]);
+                    }
+                    result[l].widgetLayouts = layout.widgets;
+                    result[l].gridSettings = layout.gridSettings;
+                }
+            }
+            return result;
+        } else {
+            return null;
+        }
+    }
+
+    function setLayouts(dashboard, targetState, newLayouts) {
+        var dashboardConfiguration = dashboard.configuration;
+        var states = dashboardConfiguration.states;
+        var state = states[targetState];
+        var addedCount = 0;
+        var removedCount = 0;
+        for (var l in state.layouts) {
+            if (!newLayouts[l]) {
+                removedCount++;
+            }
+        }
+        for (l in newLayouts) {
+            if (!state.layouts[l]) {
+                addedCount++;
+            }
+        }
+        state.layouts = newLayouts;
+        var layoutsCount = Object.keys(state.layouts).length;
+        var newColumns;
+        if (addedCount) {
+            for (l in state.layouts) {
+                newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount;
+                state.layouts[l].gridSettings.columns = newColumns;
+            }
+        }
+        if (removedCount) {
+            for (l in state.layouts) {
+                newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount;
+                state.layouts[l].gridSettings.columns = newColumns;
+            }
+        }
+        removeUnusedWidgets(dashboard);
+    }
+
+    function updateLayoutSettings(layout, gridSettings) {
+        var prevGridSettings = layout.gridSettings;
+        var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
+        var ratio = gridSettings.columns / prevColumns;
+        layout.gridSettings = gridSettings;
+        for (var w in layout.widgets) {
+            var widget = layout.widgets[w];
+            widget.sizeX = Math.round(widget.sizeX * ratio);
+            widget.sizeY = Math.round(widget.sizeY * ratio);
+            widget.col = Math.round(widget.col * ratio);
+            widget.row = Math.round(widget.row * ratio);
+        }
+    }
+
+    function addWidgetToLayout(dashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column) {
+        var dashboardConfiguration = dashboard.configuration;
+        var states = dashboardConfiguration.states;
+        var state = states[targetState];
+        var layout = state.layouts[targetLayout];
+        var layoutCount = Object.keys(state.layouts).length;
+        if (!widget.id) {
+            widget.id = utils.guid();
+        }
+        if (!dashboardConfiguration.widgets[widget.id]) {
+            dashboardConfiguration.widgets[widget.id] = widget;
+        }
+        var widgetLayout = {
+            sizeX: originalSize ? originalSize.sizeX : widget.sizeX,
+            sizeY: originalSize ? originalSize.sizeY : widget.sizeY,
+            mobileOrder: widget.config.mobileOrder,
+            mobileHeight: widget.config.mobileHeight
+        };
+
+        if (angular.isUndefined(originalColumns)) {
+            originalColumns = 24;
+        }
+
+        var gridSettings = layout.gridSettings;
+        var columns = 24;
+        if (gridSettings && gridSettings.columns) {
+            columns = gridSettings.columns;
+        }
+
+        columns = columns * layoutCount;
+
+        if (columns != originalColumns) {
+            var ratio = columns / originalColumns;
+            widgetLayout.sizeX *= ratio;
+            widgetLayout.sizeY *= ratio;
+        }
+
+        if (row > -1 && column > - 1) {
+            widgetLayout.row = row;
+            widgetLayout.col = column;
+        } else {
+            row = 0;
+            for (var w in layout.widgets) {
+                var existingLayout = layout.widgets[w];
+                var wRow = existingLayout.row ? existingLayout.row : 0;
+                var wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1;
+                var bottom = wRow + wSizeY;
+                row = Math.max(row, bottom);
+            }
+            widgetLayout.row = row;
+            widgetLayout.col = 0;
+        }
+
+        layout.widgets[widget.id] = widgetLayout;
+    }
+
+    function removeWidgetFromLayout(dashboard, targetState, targetLayout, widgetId) {
+        var dashboardConfiguration = dashboard.configuration;
+        var states = dashboardConfiguration.states;
+        var state = states[targetState];
+        var layout = state.layouts[targetLayout];
+        delete layout.widgets[widgetId];
+        removeUnusedWidgets(dashboard);
+    }
+
+    function isSingleLayoutDashboard(dashboard) {
+        var dashboardConfiguration = dashboard.configuration;
+        var states = dashboardConfiguration.states;
+        var stateKeys = Object.keys(states);
+        if (stateKeys.length === 1) {
+            var state = states[stateKeys[0]];
+            var layouts = state.layouts;
+            var layoutKeys = Object.keys(layouts);
+            if (layoutKeys.length === 1) {
+                return {
+                    state: stateKeys[0],
+                    layout: layoutKeys[0]
+                }
+            }
+        }
+        return null;
+    }
+
+    function removeUnusedWidgets(dashboard) {
+        var dashboardConfiguration = dashboard.configuration;
+        var states = dashboardConfiguration.states;
+        var widgets = dashboardConfiguration.widgets;
+        for (var widgetId in widgets) {
+            var found = false;
+            for (var s in states) {
+                var state = states[s];
+                for (var l in state.layouts) {
+                    var layout = state.layouts[l];
+                    if (layout.widgets[widgetId]) {
+                        found = true;
+                        break;
+                    }
+                }
+            }
+            if (!found) {
+                delete dashboardConfiguration.widgets[widgetId];
+            }
+
+        }
+    }
+
+    function getWidgetsArray(dashboard) {
+        var widgetsArray = [];
+        var dashboardConfiguration = dashboard.configuration;
+        var widgets = dashboardConfiguration.widgets;
+        for (var widgetId in widgets) {
+            var widget = widgets[widgetId];
+            widgetsArray.push(widget);
+        }
+        return widgetsArray;
+    }
 }
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index ae3556f..47ac7a0 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -98,7 +98,18 @@ 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",
+                to: "TO"
+            },
+            entityRelationType: {
+                contains: "Contains",
+                manages: "Manages"
             },
             eventType: {
                 alarm: {
@@ -199,6 +210,9 @@ export default angular.module('thingsboard.types', [])
             systemBundleAlias: {
                 charts: "charts",
                 cards: "cards"
+            },
+            translate: {
+                dashboardStatePrefix: "dashboardState.state."
             }
         }
     ).name;
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.directive.js b/ui/src/app/components/dashboard.directive.js
index 93b889a..b254cca 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -51,7 +51,9 @@ function Dashboard() {
         scope: true,
         bindToController: {
             widgets: '=',
+            widgetLayouts: '=?',
             aliasesInfo: '=',
+            stateController: '=',
             dashboardTimewindow: '=?',
             columns: '=',
             margins: '=',
@@ -73,7 +75,8 @@ function Dashboard() {
             onInit: '&?',
             onInitFailed: '&?',
             dashboardStyle: '=?',
-            dashboardClass: '=?'
+            dashboardClass: '=?',
+            ignoreLoading: '=?'
         },
         controller: DashboardController,
         controllerAs: 'vm',
@@ -82,7 +85,7 @@ function Dashboard() {
 }
 
 /*@ngInject*/
-function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types) {
+function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types, utils) {
 
     var highlightedMode = false;
     var highlightedWidget = null;
@@ -132,14 +135,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
 
     updateMobileOpts();
 
+    vm.widgetLayoutInfo = {
+    };
+
     vm.widgetItemMap = {
+        sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
+        sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
+        row: 'vm.widgetLayoutInfo[widget.id].row',
+        col: 'vm.widgetLayoutInfo[widget.id].col',
+        minSizeY: 'widget.minSizeY',
+        maxSizeY: 'widget.maxSizeY'
+    };
+
+    /*vm.widgetItemMap = {
         sizeX: 'vm.widgetSizeX(widget)',
         sizeY: 'vm.widgetSizeY(widget)',
-        row: 'widget.row',
-        col: 'widget.col',
+        row: 'vm.widgetRow(widget)',
+        col: 'vm.widgetCol(widget)',
         minSizeY: 'widget.minSizeY',
         maxSizeY: 'widget.maxSizeY'
-    };
+    };*/
 
     vm.isWidgetExpanded = false;
     vm.isHighlighted = isHighlighted;
@@ -156,6 +171,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
 
     vm.widgetSizeX = widgetSizeX;
     vm.widgetSizeY = widgetSizeY;
+    vm.widgetRow = widgetRow;
+    vm.widgetCol = widgetCol;
     vm.widgetColor = widgetColor;
     vm.widgetBackgroundColor = widgetBackgroundColor;
     vm.widgetPadding = widgetPadding;
@@ -173,6 +190,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
     vm.openWidgetContextMenu = openWidgetContextMenu;
 
     vm.getEventGridPosition = getEventGridPosition;
+    vm.reload = reload;
 
     vm.contextMenuItems = [];
     vm.contextMenuEvent = null;
@@ -201,6 +219,45 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
         }
     };
 
+    $scope.$watchCollection('vm.widgets', function () {
+        var ids = [];
+        for (var i=0;i<vm.widgets.length;i++) {
+            var widget = vm.widgets[i];
+            if (!widget.id) {
+                widget.id = utils.guid();
+            }
+            ids.push(widget.id);
+            var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
+            if (!layoutInfoObject) {
+                layoutInfoObject = {
+                    widget: widget
+                };
+                Object.defineProperty(layoutInfoObject, 'sizeX', {
+                    get: function() { return widgetSizeX(this.widget) },
+                    set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
+                });
+                Object.defineProperty(layoutInfoObject, 'sizeY', {
+                    get: function() { return widgetSizeY(this.widget) },
+                    set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
+                });
+                Object.defineProperty(layoutInfoObject, 'row', {
+                    get: function() { return widgetRow(this.widget) },
+                    set: function(newRow) { setWidgetRow(this.widget, newRow)}
+                });
+                Object.defineProperty(layoutInfoObject, 'col', {
+                    get: function() { return widgetCol(this.widget) },
+                    set: function(newCol) { setWidgetCol(this.widget, newCol)}
+                });
+                vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
+            }
+        }
+        for (var widgetId in vm.widgetLayoutInfo) {
+            if (ids.indexOf(widgetId) === -1) {
+                delete vm.widgetLayoutInfo[widgetId];
+            }
+        }
+    });
+
     //TODO: widgets visibility
     /*gridsterParent.scroll(function () {
         updateVisibleRect();
@@ -281,6 +338,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
     $scope.$on('gridster-resized', function (event, sizes, theGridster) {
         if (checkIsLocalGridsterElement(theGridster)) {
             vm.gridster = theGridster;
+            vm.isResizing = false;
             //TODO: widgets visibility
             //updateVisibleRect(false, true);
         }
@@ -304,20 +362,22 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
         }
     });
 
+    function widgetOrder(widget) {
+        var order;
+        if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+            order = vm.widgetLayouts[widget.id].mobileOrder;
+        } else if (widget.config.mobileOrder) {
+            order = widget.config.mobileOrder;
+        } else {
+            order = widget.row;
+        }
+        return order;
+    }
+
     $scope.$on('widgetPositionChanged', function () {
         vm.widgets.sort(function (widget1, widget2) {
-            var row1;
-            var row2;
-            if (angular.isDefined(widget1.config.mobileOrder)) {
-                row1 = widget1.config.mobileOrder;
-            } else {
-                row1 = widget1.row;
-            }
-            if (angular.isDefined(widget2.config.mobileOrder)) {
-                row2 = widget2.config.mobileOrder;
-            } else {
-                row2 = widget2.row;
-            }
+            var row1 = widgetOrder(widget1);
+            var row2 = widgetOrder(widget2);
             var res = row1 - row2;
             if (res === 0) {
                 res = widget1.col - widget2.col;
@@ -328,6 +388,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
 
     loadStDiff();
 
+    function reload() {
+        loadStDiff();
+    }
+
     function loadStDiff() {
         if (vm.getStDiff) {
             var promise = vm.getStDiff();
@@ -570,18 +634,89 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
     }
 
     function widgetSizeX(widget) {
-        return widget.sizeX;
+        if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+            return vm.widgetLayouts[widget.id].sizeX;
+        } else {
+            return widget.sizeX;
+        }
+    }
+
+    function setWidgetSizeX(widget, sizeX) {
+        if (!vm.gridsterOpts.isMobile) {
+            if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+                vm.widgetLayouts[widget.id].sizeX = sizeX;
+            } else {
+                widget.sizeX = sizeX;
+            }
+        }
     }
 
     function widgetSizeY(widget) {
         if (vm.gridsterOpts.isMobile) {
-            if (widget.config.mobileHeight) {
-                return widget.config.mobileHeight;
+            var mobileHeight;
+            if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+                mobileHeight = vm.widgetLayouts[widget.id].mobileHeight;
+            }
+            if (!mobileHeight && widget.config.mobileHeight) {
+                mobileHeight = widget.config.mobileHeight;
+            }
+            if (mobileHeight) {
+                return mobileHeight;
             } else {
                 return widget.sizeY * 24 / vm.gridsterOpts.columns;
             }
         } else {
-            return widget.sizeY;
+            if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+                return vm.widgetLayouts[widget.id].sizeY;
+            } else {
+                return widget.sizeY;
+            }
+        }
+    }
+
+    function setWidgetSizeY(widget, sizeY) {
+        if (!vm.gridsterOpts.isMobile) {
+            if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+                vm.widgetLayouts[widget.id].sizeY = sizeY;
+            } else {
+                widget.sizeY = sizeY;
+            }
+        }
+    }
+
+    function widgetRow(widget) {
+        if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+            return vm.widgetLayouts[widget.id].row;
+        } else {
+            return widget.row;
+        }
+    }
+
+    function setWidgetRow(widget, row) {
+        if (!vm.gridsterOpts.isMobile) {
+            if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+                vm.widgetLayouts[widget.id].row = row;
+            } else {
+                widget.row = row;
+            }
+        }
+    }
+
+    function widgetCol(widget) {
+        if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+            return vm.widgetLayouts[widget.id].col;
+        } else {
+            return widget.col;
+        }
+    }
+
+    function setWidgetCol(widget, col) {
+        if (!vm.gridsterOpts.isMobile) {
+            if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+                vm.widgetLayouts[widget.id].col = col;
+            } else {
+                widget.col = col;
+            }
         }
     }
 
@@ -655,7 +790,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
             var maxRows = vm.gridsterOpts.maxRows;
             for (var i = 0; i < vm.widgets.length; i++) {
                 var w = vm.widgets[i];
-                var bottom = w.row + w.sizeY;
+                var bottom = widgetRow(w) + widgetSizeY(w);
                 maxRows = Math.max(maxRows, bottom);
             }
             vm.gridsterOpts.maxRows = Math.max(maxRows, vm.gridsterOpts.maxRows);
@@ -664,7 +799,11 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
 
     function dashboardLoaded() {
         $timeout(function () {
-            $scope.$watch('vm.dashboardTimewindow', function () {
+            if (vm.dashboardTimewindowWatch) {
+                vm.dashboardTimewindowWatch();
+                vm.dashboardTimewindowWatch = null;
+            }
+            vm.dashboardTimewindowWatch = $scope.$watch('vm.dashboardTimewindow', function () {
                 $scope.$broadcast('dashboardTimewindowChanged', vm.dashboardTimewindow);
             }, true);
             adoptMaxRows();
@@ -680,7 +819,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
     }
 
     function loading() {
-        return $rootScope.loading;
+        return !vm.ignoreLoading && $rootScope.loading;
     }
 
 }
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 3d11e46..69934ac 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -16,8 +16,10 @@
 
 -->
 <md-content flex layout="column" class="tb-progress-cover" layout-align="center center"
-   ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
-	<md-progress-circular md-mode="indeterminate" ng-disabled="!vm.loading() && !vm.dashboardLoading || vm.isEdit" class="md-warn" md-diameter="100"></md-progress-circular>
+			ng-style="vm.dashboardStyle"
+   ng-show="((vm.loading() || vm.dashboardLoading) && !vm.isEdit) || vm.isResizing">
+	<md-progress-circular md-mode="indeterminate" ng-disabled="(!vm.loading() && !vm.dashboardLoading || vm.isEdit) && !vm.isResizing" class="md-warn" md-diameter="100">
+	</md-progress-circular>
 </md-content>
 <md-menu md-position-mode="target target" tb-mousepoint-menu>
 	<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
@@ -88,6 +90,7 @@
 										 locals="{ visibleRect: vm.visibleRect,
 										 widget: widget,
 										 aliasesInfo: vm.aliasesInfo,
+										 stateController: vm.stateController,
 										 isEdit: vm.isEdit,
 										 stDiff: vm.stDiff,
 										 dashboardTimewindow: vm.dashboardTimewindow,
diff --git a/ui/src/app/components/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js
index afa5f56..77e3c1c 100644
--- a/ui/src/app/components/dashboard-autocomplete.directive.js
+++ b/ui/src/app/components/dashboard-autocomplete.directive.js
@@ -53,7 +53,15 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
                     promise = $q.when({data: []});
                 }
             } else {
-                promise = dashboardService.getTenantDashboards(pageLink);
+                if (userService.getAuthority() === 'SYS_ADMIN') {
+                    if (scope.tenantId) {
+                        promise = dashboardService.getTenantDashboardsByTenantId(scope.tenantId, pageLink);
+                    } else {
+                        promise = $q.when({data: []});
+                    }
+                } else {
+                    promise = dashboardService.getTenantDashboards(pageLink);
+                }
             }
 
             promise.then(function success(result) {
@@ -76,7 +84,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
 
         ngModelCtrl.$render = function () {
             if (ngModelCtrl.$viewValue) {
-                dashboardService.getDashboard(ngModelCtrl.$viewValue).then(
+                dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then(
                     function success(dashboard) {
                         scope.dashboard = dashboard;
                     },
@@ -117,6 +125,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
         link: linker,
         scope: {
             dashboardsScope: '@',
+            tenantId: '=',
             customerId: '=',
             theForm: '=?',
             tbRequired: '=?',
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/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js
index 9b3081e..90810af 100644
--- a/ui/src/app/components/datakey-config.directive.js
+++ b/ui/src/app/components/datakey-config.directive.js
@@ -76,6 +76,8 @@ function DatakeyConfig($compile, $templateCache, $q, types) {
                 scope.model.name = ngModelCtrl.$viewValue.name;
                 scope.model.label = ngModelCtrl.$viewValue.label;
                 scope.model.color = ngModelCtrl.$viewValue.color;
+                scope.model.units = ngModelCtrl.$viewValue.units;
+                scope.model.decimals = ngModelCtrl.$viewValue.decimals;
                 scope.model.funcBody = ngModelCtrl.$viewValue.funcBody;
                 scope.model.postFuncBody = ngModelCtrl.$viewValue.postFuncBody;
                 scope.model.usePostProcessing = scope.model.postFuncBody ? true : false;
@@ -97,6 +99,8 @@ function DatakeyConfig($compile, $templateCache, $q, types) {
                 value.name = scope.model.name;
                 value.label = scope.model.label;
                 value.color = scope.model.color;
+                value.units = scope.model.units;
+                value.decimals = scope.model.decimals;
                 value.funcBody = scope.model.funcBody;
                 if (!scope.model.postFuncBody) {
                     delete value.postFuncBody;
diff --git a/ui/src/app/components/datakey-config.tpl.html b/ui/src/app/components/datakey-config.tpl.html
index f170220..755c4f4 100644
--- a/ui/src/app/components/datakey-config.tpl.html
+++ b/ui/src/app/components/datakey-config.tpl.html
@@ -48,6 +48,16 @@
 		    md-color-history="false">
 		</div>    
 	</div>
+	<div layout="row" layout-align="start center">
+		<md-input-container flex>
+			<label translate>datakey.units</label>
+			<input name="units" ng-model="model.units">
+		</md-input-container>
+		<md-input-container flex>
+			<label translate>datakey.decimals</label>
+			<input name="decimals" ng-model="model.decimals" type="number" min="0" max="15" step="1" ng-pattern="/^\d*$/">
+		</md-input-container>
+	</div>
 	<section layout="column" ng-if="model.type === types.dataKeyType.function">
 		<span translate>datakey.data-generation-func</span>
 		<br/>
diff --git a/ui/src/app/components/expand-fullscreen.directive.js b/ui/src/app/components/expand-fullscreen.directive.js
index 37491e7..b70fd94 100644
--- a/ui/src/app/components/expand-fullscreen.directive.js
+++ b/ui/src/app/components/expand-fullscreen.directive.js
@@ -24,7 +24,7 @@ export default angular.module('thingsboard.directives.expandFullscreen', [])
 /* eslint-disable angular/angularelement */
 
 /*@ngInject*/
-function ExpandFullscreen($compile, $document) {
+function ExpandFullscreen($compile, $document, $timeout) {
 
     var uniqueId = 1;
     var linker = function (scope, element, attrs) {
@@ -97,10 +97,6 @@ function ExpandFullscreen($compile, $document) {
             scope.expanded = !scope.expanded;
         }
 
-        var expandButton = null;
-        if (attrs.expandButtonId) {
-            expandButton = $('#' + attrs.expandButtonId, element)[0];
-        }
         var buttonSize;
         if (attrs.expandButtonSize) {
             buttonSize = attrs.expandButtonSize;
@@ -115,27 +111,38 @@ function ExpandFullscreen($compile, $document) {
             'options=\'{"easing": "circ-in-out", "duration": 375, "rotation": "none"}\'>' +
             '</ng-md-icon>';
 
-        if (expandButton) {
-            expandButton = angular.element(expandButton);
-            if (scope.hideExpandButton()) {
-                expandButton.remove();
-            } else {
-                expandButton.attr('md-ink-ripple', 'false');
-                expandButton.append(html);
+        if (attrs.expandButtonId) {
+            $timeout(function() {
+               var expandButton = $('#' + attrs.expandButtonId, element)[0];
+                renderExpandButton(expandButton);
+            });
+        } else {
+            renderExpandButton();
+        }
+
+        function renderExpandButton(expandButton) {
+            if (expandButton) {
+                expandButton = angular.element(expandButton);
+                if (scope.hideExpandButton()) {
+                    expandButton.remove();
+                } else {
+                    expandButton.attr('md-ink-ripple', 'false');
+                    expandButton.append(html);
 
-                $compile(expandButton.contents())(scope);
+                    $compile(expandButton.contents())(scope);
 
-                expandButton.on("click", scope.toggleExpand);
-            }
-        } else if (!scope.hideExpandButton()) {
-            var button = angular.element('<md-button class="tb-fullscreen-button-style tb-fullscreen-button-pos md-icon-button" ' +
-                'md-ink-ripple="false" ng-click="toggleExpand($event)">' +
-                html +
-                '</md-button>');
+                    expandButton.on("click", scope.toggleExpand);
+                }
+            } else if (!scope.hideExpandButton()) {
+                var button = angular.element('<md-button class="tb-fullscreen-button-style tb-fullscreen-button-pos md-icon-button" ' +
+                    'md-ink-ripple="false" ng-click="toggleExpand($event)">' +
+                    html +
+                    '</md-button>');
 
-            $compile(button)(scope);
+                $compile(button)(scope);
 
-            element.prepend(button);
+                element.prepend(button);
+            }
         }
     }
 
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index 5664400..296456a 100644
--- a/ui/src/app/components/grid.directive.js
+++ b/ui/src/app/components/grid.directive.js
@@ -197,7 +197,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         },
 
         getLength: function () {
-            if (vm.items.hasNext) {
+            if (vm.items.hasNext && !vm.items.pending) {
                 return vm.items.rowData.length + pageSize;
             } else {
                 return vm.items.rowData.length;
@@ -206,7 +206,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
 
         fetchMoreItems_: function () {
             if (vm.items.hasNext && !vm.items.pending) {
-                var promise = vm.fetchItemsFunc(vm.items.nextPageLink);
+                var promise = vm.fetchItemsFunc(vm.items.nextPageLink, $scope.searchConfig.searchEntitySubtype);
                 if (promise) {
                     vm.items.pending = true;
                     promise.then(
@@ -433,6 +433,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         reload();
     });
 
+    $scope.$on('searchEntitySubtypeUpdated', function () {
+        reload();
+    });
+
     vm.onGridInited(vm);
 
     vm.itemRows.getItemAtIndex(pageSize);
@@ -441,18 +445,16 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         if (vm.items && vm.items.pending) {
             vm.items.reloadPending = true;
         } else {
-            vm.items = {
-                data: [],
-                rowData: [],
-                nextPageLink: {
-                    limit: pageSize,
-                    textSearch: $scope.searchConfig.searchText
-                },
-                selections: {},
-                selectedCount: 0,
-                hasNext: true,
-                pending: false
+            vm.items.data.length = 0;
+            vm.items.rowData.length = 0;
+            vm.items.nextPageLink = {
+                limit: pageSize,
+                textSearch: $scope.searchConfig.searchText
             };
+            vm.items.selections = {};
+            vm.items.selectedCount = 0;
+            vm.items.hasNext = true;
+            vm.items.pending = false;
             vm.detailsConfig.isDetailsOpen = false;
             vm.items.reloadPending = false;
             vm.itemRows.getItemAtIndex(pageSize);
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
index c29c2e5..24285d8 100644
--- a/ui/src/app/components/grid.tpl.html
+++ b/ui/src/app/components/grid.tpl.html
@@ -24,9 +24,8 @@
         <md-virtual-repeat-container ng-show="vm.hasData()" tb-scope-element="repeatContainer" id="tb-vertical-container" md-top-index="vm.topIndex" flex>
             <div class="md-padding" layout="column">
                 <section layout="row"  md-virtual-repeat="rowItem in vm.itemRows" md-on-demand md-item-size="vm.itemHeight">
-                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}">
-                        <md-card ng-if="rowItem[n]"
-                                 ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
+                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="rowItem[n]">
+                        <md-card ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
                                  class="repeated-item tb-card-item" ng-style="{'height':(vm.itemHeight-16)+'px','cursor':'pointer'}"
                                  ng-click="vm.clickItemFunc($event, rowItem[n])">
                             <section layout="row" layout-wrap>
@@ -43,7 +42,7 @@
                                 </md-card-title>
                             </section>
                             <md-card-content flex>
-                                <tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
+                                <tb-grid-card-content flex grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
                             </md-card-content>
                             <md-card-actions layout="row" layout-align="end end">
                                 <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
@@ -56,6 +55,8 @@
                             </md-card-actions>
                         </md-card>
                     </div>
+                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="!rowItem[n]">
+                    </div>
                 </section>
             </div>
         </md-virtual-repeat-container>
diff --git a/ui/src/app/components/legend.directive.js b/ui/src/app/components/legend.directive.js
index 196629c..39c8e58 100644
--- a/ui/src/app/components/legend.directive.js
+++ b/ui/src/app/components/legend.directive.js
@@ -45,7 +45,7 @@ function Legend($compile, $templateCache, types) {
             scope.legendConfig.position === types.position.top.value;
 
         scope.toggleHideData = function(index) {
-            scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
+            scope.legendData.keys[index].dataKey.hidden = !scope.legendData.keys[index].dataKey.hidden;
         }
 
         $compile(element.contents())(scope);
diff --git a/ui/src/app/components/legend.tpl.html b/ui/src/app/components/legend.tpl.html
index 8350320..18c9137 100644
--- a/ui/src/app/components/legend.tpl.html
+++ b/ui/src/app/components/legend.tpl.html
@@ -30,7 +30,7 @@
             <td><span class="tb-legend-line"  ng-style="{backgroundColor: legendKey.dataKey.color}"></span></td>
             <td class="tb-legend-label"
                 ng-click="toggleHideData(legendKey.dataIndex)"
-                ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }">
+                ng-class="{ 'tb-hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden, 'tb-horizontal': isHorizontal }">
                 {{ legendKey.dataKey.label }}
             </td>
             <td class="tb-legend-value" ng-if="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td>
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/components/related-entity-autocomplete.directive.js b/ui/src/app/components/related-entity-autocomplete.directive.js
new file mode 100644
index 0000000..f93ded2
--- /dev/null
+++ b/ui/src/app/components/related-entity-autocomplete.directive.js
@@ -0,0 +1,128 @@
+/*
+ * 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 './related-entity-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import relatedEntityAutocompleteTemplate from './related-entity-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+export default angular.module('thingsboard.directives.relatedEntityAutocomplete', [])
+    .directive('tbRelatedEntityAutocomplete', RelatedEntityAutocomplete)
+    .name;
+
+/*@ngInject*/
+function RelatedEntityAutocomplete($compile, $templateCache, $q, $filter, entityService) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(relatedEntityAutocompleteTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.entity = null;
+        scope.entitySearchText = '';
+
+        scope.allEntities = null;
+
+        scope.fetchEntities = function(searchText) {
+            var deferred = $q.defer();
+            if (!scope.allEntities) {
+                entityService.getRelatedEntities(scope.rootEntityId, scope.entityType, scope.entitySubtypes, -1, []).then(
+                    function success(entities) {
+                        if (scope.excludeEntityId) {
+                            var result = $filter('filter')(entities, {id: {id: scope.excludeEntityId.id} }, true);
+                            result = $filter('filter')(result, {id: {entityType: scope.excludeEntityId.entityType} }, true);
+                            if (result && result.length) {
+                                var excludeEntity = result[0];
+                                var index = entities.indexOf(excludeEntity);
+                                if (index > -1) {
+                                    entities.splice(index, 1);
+                                }
+                            }
+                        }
+                        scope.allEntities = entities;
+                        filterEntities(searchText, deferred);
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            } else {
+                filterEntities(searchText, deferred);
+            }
+            return deferred.promise;
+        }
+
+        function filterEntities(searchText, deferred) {
+            var result = $filter('filter')(scope.allEntities, {name: searchText});
+            deferred.resolve(result);
+        }
+
+        scope.entitySearchTextChanged = function() {
+        }
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id : null);
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                entityService.getRelatedEntity(ngModelCtrl.$viewValue).then(
+                    function success(entity) {
+                        scope.entity = entity;
+                    },
+                    function fail() {
+                        scope.entity = null;
+                    }
+                );
+            } else {
+                scope.entity = null;
+            }
+        }
+
+        scope.$watch('entity', function () {
+            scope.updateView();
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            rootEntityId: '=',
+            entityType: '=',
+            entitySubtypes: '=',
+            excludeEntityId: '=?',
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled',
+            placeholderText: '@',
+            notFoundText: '@',
+            requiredText: '@'
+        }
+    };
+}
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index 42299c6..ef53ac2 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -22,7 +22,7 @@ import Subscription from '../api/subscription';
 /*@ngInject*/
 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
                                          datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
-                                         dashboardTimewindowApi, widget, aliasesInfo, widgetType) {
+                                         dashboardTimewindowApi, widget, aliasesInfo, stateController, widgetType) {
 
     var vm = this;
 
@@ -131,7 +131,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         },
         utils: {
             formatValue: formatValue
-        }
+        },
+        stateController: stateController
     };
 
     var subscriptionContext = {
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index 6c7c472..c3793be 100644
--- a/ui/src/app/components/widget-config.directive.js
+++ b/ui/src/app/components/widget-config.directive.js
@@ -89,58 +89,68 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
 
         ngModelCtrl.$render = function () {
             if (ngModelCtrl.$viewValue) {
-                scope.selectedTab = 0;
-                scope.title = ngModelCtrl.$viewValue.title;
-                scope.showTitle = ngModelCtrl.$viewValue.showTitle;
-                scope.dropShadow = angular.isDefined(ngModelCtrl.$viewValue.dropShadow) ? ngModelCtrl.$viewValue.dropShadow : true;
-                scope.enableFullscreen = angular.isDefined(ngModelCtrl.$viewValue.enableFullscreen) ? ngModelCtrl.$viewValue.enableFullscreen : true;
-                scope.backgroundColor = ngModelCtrl.$viewValue.backgroundColor;
-                scope.color = ngModelCtrl.$viewValue.color;
-                scope.padding = ngModelCtrl.$viewValue.padding;
-                scope.titleStyle =
-                    angular.toJson(angular.isDefined(ngModelCtrl.$viewValue.titleStyle) ? ngModelCtrl.$viewValue.titleStyle : {
-                        fontSize: '16px',
-                        fontWeight: 400
-                    }, true);
-                scope.mobileOrder = ngModelCtrl.$viewValue.mobileOrder;
-                scope.mobileHeight = ngModelCtrl.$viewValue.mobileHeight;
-                scope.units = ngModelCtrl.$viewValue.units;
-                scope.decimals = ngModelCtrl.$viewValue.decimals;
-                scope.useDashboardTimewindow = angular.isDefined(ngModelCtrl.$viewValue.useDashboardTimewindow) ?
-                    ngModelCtrl.$viewValue.useDashboardTimewindow : true;
-                scope.timewindow = ngModelCtrl.$viewValue.timewindow;
-                scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ?
-                    ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
-                scope.legendConfig = ngModelCtrl.$viewValue.legendConfig;
-                if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
-                    && scope.isDataEnabled) {
-                    if (scope.datasources) {
-                        scope.datasources.splice(0, scope.datasources.length);
-                    } else {
-                        scope.datasources = [];
-                    }
-                    if (ngModelCtrl.$viewValue.datasources) {
-                        for (var i in ngModelCtrl.$viewValue.datasources) {
-                            scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
+                var config = ngModelCtrl.$viewValue.config;
+                var layout = ngModelCtrl.$viewValue.layout;
+                if (config) {
+                    scope.selectedTab = 0;
+                    scope.title = config.title;
+                    scope.showTitle = config.showTitle;
+                    scope.dropShadow = angular.isDefined(config.dropShadow) ? config.dropShadow : true;
+                    scope.enableFullscreen = angular.isDefined(config.enableFullscreen) ? config.enableFullscreen : true;
+                    scope.backgroundColor = config.backgroundColor;
+                    scope.color = config.color;
+                    scope.padding = config.padding;
+                    scope.titleStyle =
+                        angular.toJson(angular.isDefined(config.titleStyle) ? config.titleStyle : {
+                            fontSize: '16px',
+                            fontWeight: 400
+                        }, true);
+                    scope.units = config.units;
+                    scope.decimals = config.decimals;
+                    scope.useDashboardTimewindow = angular.isDefined(config.useDashboardTimewindow) ?
+                        config.useDashboardTimewindow : true;
+                    scope.timewindow = config.timewindow;
+                    scope.showLegend = angular.isDefined(config.showLegend) ?
+                        config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
+                    scope.legendConfig = config.legendConfig;
+                    if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
+                        && scope.isDataEnabled) {
+                        if (scope.datasources) {
+                            scope.datasources.splice(0, scope.datasources.length);
+                        } else {
+                            scope.datasources = [];
                         }
-                    }
-                } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
-                    if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
-                        var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
-                        if (scope.entityAliases[aliasId]) {
-                            scope.targetDeviceAlias.value = {id: aliasId, alias: scope.entityAliases[aliasId].alias,
-                                entityType: scope.entityAliases[aliasId].entityType, entityId: scope.entityAliases[aliasId].entityId};
+                        if (config.datasources) {
+                            for (var i in config.datasources) {
+                                scope.datasources.push({value: config.datasources[i]});
+                            }
+                        }
+                    } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
+                        if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
+                            var aliasId = config.targetDeviceAliasIds[0];
+                            if (scope.entityAliases[aliasId]) {
+                                scope.targetDeviceAlias.value = {
+                                    id: aliasId,
+                                    alias: scope.entityAliases[aliasId].alias,
+                                    entityType: scope.entityAliases[aliasId].entityType,
+                                    entityId: scope.entityAliases[aliasId].entityId
+                                };
+                            } else {
+                                scope.targetDeviceAlias.value = null;
+                            }
                         } else {
                             scope.targetDeviceAlias.value = null;
                         }
-                    } else {
-                        scope.targetDeviceAlias.value = null;
                     }
-                }
 
-                scope.settings = ngModelCtrl.$viewValue.settings;
+                    scope.settings = config.settings;
 
-                scope.updateSchemaForm();
+                    scope.updateSchemaForm();
+                }
+                if (layout) {
+                    scope.mobileOrder = layout.mobileOrder;
+                    scope.mobileHeight = layout.mobileHeight;
+                }
             }
         };
 
@@ -163,19 +173,22 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
         scope.updateValidity = function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
-                var valid;
-                if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
-                    valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
-                    ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
-                } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
-                    valid = value && value.datasources && value.datasources.length > 0;
-                    ngModelCtrl.$setValidity('datasources', valid);
-                }
-                try {
-                    angular.fromJson(scope.titleStyle);
-                    ngModelCtrl.$setValidity('titleStyle', true);
-                } catch (e) {
-                    ngModelCtrl.$setValidity('titleStyle', false);
+                var config = value.config;
+                if (config) {
+                    var valid;
+                    if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
+                        valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0;
+                        ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
+                    } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
+                        valid = config && config.datasources && config.datasources.length > 0;
+                        ngModelCtrl.$setValidity('datasources', valid);
+                    }
+                    try {
+                        angular.fromJson(scope.titleStyle);
+                        ngModelCtrl.$setValidity('titleStyle', true);
+                    } catch (e) {
+                        ngModelCtrl.$setValidity('titleStyle', false);
+                    }
                 }
             }
         };
@@ -184,24 +197,30 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
             'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
-                value.title = scope.title;
-                value.showTitle = scope.showTitle;
-                value.dropShadow = scope.dropShadow;
-                value.enableFullscreen = scope.enableFullscreen;
-                value.backgroundColor = scope.backgroundColor;
-                value.color = scope.color;
-                value.padding = scope.padding;
-                try {
-                    value.titleStyle = angular.fromJson(scope.titleStyle);
-                } catch (e) {
-                    value.titleStyle = {};
+                if (value.config) {
+                    var config = value.config;
+                    config.title = scope.title;
+                    config.showTitle = scope.showTitle;
+                    config.dropShadow = scope.dropShadow;
+                    config.enableFullscreen = scope.enableFullscreen;
+                    config.backgroundColor = scope.backgroundColor;
+                    config.color = scope.color;
+                    config.padding = scope.padding;
+                    try {
+                        config.titleStyle = angular.fromJson(scope.titleStyle);
+                    } catch (e) {
+                        config.titleStyle = {};
+                    }
+                    config.units = scope.units;
+                    config.decimals = scope.decimals;
+                    config.useDashboardTimewindow = scope.useDashboardTimewindow;
+                    config.showLegend = scope.showLegend;
+                }
+                if (value.layout) {
+                    var layout = value.layout;
+                    layout.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined;
+                    layout.mobileHeight = scope.mobileHeight;
                 }
-                value.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined;
-                value.mobileHeight = scope.mobileHeight;
-                value.units = scope.units;
-                value.decimals = scope.decimals;
-                value.useDashboardTimewindow = scope.useDashboardTimewindow;
-                value.showLegend = scope.showLegend;
                 ngModelCtrl.$setViewValue(value);
                 scope.updateValidity();
             }
@@ -210,39 +229,46 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
         scope.$watch('currentSettings', function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
-                value.settings = scope.currentSettings;
-                ngModelCtrl.$setViewValue(value);
+                if (value.config) {
+                    value.config.settings = scope.currentSettings;
+                    ngModelCtrl.$setViewValue(value);
+                }
             }
         }, true);
 
         scope.$watch('timewindow', function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
-                value.timewindow = scope.timewindow;
-                ngModelCtrl.$setViewValue(value);
+                if (value.config) {
+                    value.config.timewindow = scope.timewindow;
+                    ngModelCtrl.$setViewValue(value);
+                }
             }
         }, true);
 
         scope.$watch('legendConfig', function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
-                value.legendConfig = scope.legendConfig;
-                ngModelCtrl.$setViewValue(value);
+                if (value.config) {
+                    value.config.legendConfig = scope.legendConfig;
+                    ngModelCtrl.$setViewValue(value);
+                }
             }
         }, true);
 
         scope.$watch('datasources', function () {
-            if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value
+            if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value
                 && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
                 var value = ngModelCtrl.$viewValue;
-                if (value.datasources) {
-                    value.datasources.splice(0, value.datasources.length);
+                var config = value.config;
+                if (config.datasources) {
+                    config.datasources.splice(0, config.datasources.length);
                 } else {
-                    value.datasources = [];
+                    config.datasources = [];
                 }
                 if (scope.datasources) {
                     for (var i in scope.datasources) {
-                        value.datasources.push(scope.datasources[i].value);
+                        config.datasources.push(scope.datasources[i].value);
                     }
                 }
                 ngModelCtrl.$setViewValue(value);
@@ -251,12 +277,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
         }, true);
 
         scope.$watch('targetDeviceAlias.value', function () {
-            if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
+            if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
                 var value = ngModelCtrl.$viewValue;
+                var config = value.config;
                 if (scope.targetDeviceAlias.value) {
-                    value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
+                    config.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
                 } else {
-                    value.targetDeviceAliasIds = [];
+                    config.targetDeviceAliasIds = [];
                 }
                 ngModelCtrl.$setViewValue(value);
                 scope.updateValidity();
@@ -328,10 +355,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
             var matches = false;
             do {
                 matches = false;
-                if (value.datasources) {
-                    for (var d in value.datasources) {
-                        var datasource = value.datasources[d];
-                        for (var k in datasource.dataKeys) {
+                if (value.config.datasources) {
+                    for (var d=0;d<value.config.datasources.length;d++) {
+                        var datasource = value.config.datasources[d];
+                        for (var k=0;k<datasource.dataKeys.length;k++) {
                             var dataKey = datasource.dataKeys[k];
                             if (dataKey.label === label) {
                                 i++;
@@ -348,9 +375,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
         scope.genNextColor = function () {
             var i = 0;
             var value = ngModelCtrl.$viewValue;
-            if (value.datasources) {
-                for (var d in value.datasources) {
-                    var datasource = value.datasources[d];
+            if (value.config.datasources) {
+                for (var d=0;d<value.config.datasources.length;d++) {
+                    var datasource = value.config.datasources[d];
                     i += datasource.dataKeys.length;
                 }
             }
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index de3bf95..2f23cf9 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -37,7 +37,13 @@ export default function AddWidgetController($scope, widgetService, entityService
     vm.fetchEntityKeys = fetchEntityKeys;
     vm.createEntityAlias = createEntityAlias;
 
-    vm.widgetConfig = vm.widget.config;
+    vm.widgetConfig = {
+        config: vm.widget.config,
+        layout: {}
+    };
+
+    vm.widgetConfig.layout.mobileOrder = vm.widget.config.mobileOrder;
+    vm.widgetConfig.layout.mobileHeight = vm.widget.config.mobileHeight;
 
     var settingsSchema = vm.widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
     var dataKeySettingsSchema = vm.widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
@@ -85,7 +91,9 @@ export default function AddWidgetController($scope, widgetService, entityService
     function add () {
         if ($scope.theForm.$valid) {
             $scope.theForm.$setPristine();
-            vm.widget.config = vm.widgetConfig;
+            vm.widget.config = vm.widgetConfig.config;
+            vm.widget.config.mobileOrder = vm.widgetConfig.layout.mobileOrder;
+            vm.widget.config.mobileHeight = vm.widgetConfig.layout.mobileHeight;
             $mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo});
         }
     }
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 79ee6ae..ecf072d 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -16,22 +16,27 @@
 /* eslint-disable import/no-unresolved, import/default */
 
 import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
-import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
+import dashboardSettingsTemplate from './dashboard-settings.tpl.html';
+import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html';
+import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html';
 import addWidgetTemplate from './add-widget.tpl.html';
+import selectTargetLayoutTemplate from './layouts/select-target-layout.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
 export default function DashboardController(types, dashboardUtils, widgetService, userService,
                                             dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope,
-                                            $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
+                                            $scope, $element, $state, $stateParams, $mdDialog, $mdMedia, $timeout, $document, $q, $translate, $filter) {
 
     var vm = this;
 
     vm.user = userService.getCurrentUser();
     vm.dashboard = null;
     vm.editingWidget = null;
+    vm.editingWidgetLayout = null;
     vm.editingWidgetOriginal = null;
+    vm.editingWidgetLayoutOriginal = null;
     vm.editingWidgetSubtitle = null;
     vm.forceDashboardMobileMode = false;
     vm.isAddingWidget = false;
@@ -43,8 +48,6 @@ export default function DashboardController(types, dashboardUtils, widgetService
     vm.staticWidgetTypes = [];
     vm.widgetEditMode = $state.$current.data.widgetEditMode;
     vm.iframeMode = $rootScope.iframeMode;
-    vm.widgets = [];
-    vm.dashboardInitComplete = false;
 
     vm.isToolbarOpened = false;
 
@@ -60,10 +63,35 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     Object.defineProperty(vm, 'toolbarOpened', {
-        get: function() { return vm.isToolbarOpened || vm.isEdit; },
+        get: function() {
+            return !vm.widgetEditMode &&
+                (toolbarAlwaysOpen() || $scope.forceFullscreen || vm.isToolbarOpened || vm.isEdit || vm.showRightLayoutSwitch()); },
         set: function() { }
     });
 
+    vm.layouts = {
+        main: {
+            show: false,
+            layoutCtx: {
+                id: 'main',
+                widgets: [],
+                widgetLayouts: {},
+                gridSettings: {},
+                ignoreLoading: false
+            }
+        },
+        right: {
+            show: false,
+            layoutCtx: {
+                id: 'right',
+                widgets: [],
+                widgetLayouts: {},
+                gridSettings: {},
+                ignoreLoading: false
+            }
+        }
+    };
+
     vm.openToolbar = function() {
         $timeout(function() {
             vm.isToolbarOpened = true;
@@ -76,31 +104,80 @@ export default function DashboardController(types, dashboardUtils, widgetService
         });
     }
 
+    vm.showCloseToolbar = function() {
+        return !vm.toolbarAlwaysOpen() && !$scope.forceFullscreen && !vm.isEdit && !vm.showRightLayoutSwitch();
+    }
+
+    vm.toolbarAlwaysOpen = toolbarAlwaysOpen;
+
+    vm.showRightLayoutSwitch = function() {
+        return vm.isMobile && vm.layouts.right.show;
+    }
+
+    vm.toggleLayouts = function() {
+        vm.isRightLayoutOpened = !vm.isRightLayoutOpened;
+    }
+
+    vm.openRightLayout = function() {
+        vm.isRightLayoutOpened = true;
+    }
+
+    vm.isRightLayoutOpened = false;
+    vm.isMobile = !$mdMedia('gt-sm');
+
+    $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) {
+        vm.isMobile = !isGtSm;
+    });
+
+    vm.mainLayoutWidth = function() {
+        if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') {
+            return '100%';
+        } else {
+            return vm.layouts.right.show && !vm.isMobile ? '50%' : '100%';
+        }
+    }
+
+    vm.mainLayoutHeight = function() {
+        if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') {
+            return '100%';
+        } else {
+            return 'auto';
+        }
+    }
+
+    vm.rightLayoutWidth = function() {
+        if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') {
+            return '100%';
+        } else {
+            return vm.isMobile ? '100%' : '50%';
+        }
+    }
+
+    vm.rightLayoutHeight = function() {
+        if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') {
+            return '100%';
+        } else {
+            return 'auto';
+        }
+    }
+
+    vm.getServerTimeDiff = getServerTimeDiff;
     vm.addWidget = addWidget;
     vm.addWidgetFromType = addWidgetFromType;
-    vm.dashboardInited = dashboardInited;
-    vm.dashboardInitFailed = dashboardInitFailed;
-    vm.widgetMouseDown = widgetMouseDown;
-    vm.widgetClicked = widgetClicked;
-    vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
-    vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
-    vm.editWidget = editWidget;
     vm.exportDashboard = exportDashboard;
-    vm.exportWidget = exportWidget;
     vm.importWidget = importWidget;
     vm.isPublicUser = isPublicUser;
     vm.isTenantAdmin = isTenantAdmin;
     vm.isSystemAdmin = isSystemAdmin;
-    vm.loadDashboard = loadDashboard;
-    vm.getServerTimeDiff = getServerTimeDiff;
-    vm.noData = noData;
     vm.dashboardConfigurationError = dashboardConfigurationError;
     vm.showDashboardToolbar = showDashboardToolbar;
     vm.onAddWidgetClosed = onAddWidgetClosed;
     vm.onEditWidgetClosed = onEditWidgetClosed;
+    vm.openDashboardState = openDashboardState;
     vm.openEntityAliases = openEntityAliases;
     vm.openDashboardSettings = openDashboardSettings;
-    vm.removeWidget = removeWidget;
+    vm.manageDashboardLayouts = manageDashboardLayouts;
+    vm.manageDashboardStates = manageDashboardStates;
     vm.saveDashboard = saveDashboard;
     vm.saveWidget = saveWidget;
     vm.toggleDashboardEditMode = toggleDashboardEditMode;
@@ -109,10 +186,56 @@ export default function DashboardController(types, dashboardUtils, widgetService
     vm.displayTitle = displayTitle;
     vm.displayExport = displayExport;
     vm.displayDashboardTimewindow = displayDashboardTimewindow;
+    vm.displayDashboardsSelect = displayDashboardsSelect;
     vm.displayEntitiesSelect = displayEntitiesSelect;
 
     vm.widgetsBundle;
 
+    vm.dashboardCtx = {
+        state: null,
+        stateController: {
+            openRightLayout: function() {
+                vm.openRightLayout();
+            }
+        },
+        onAddWidget: function(event, layoutCtx) {
+            addWidget(event, layoutCtx);
+        },
+        onEditWidget: function(event, layoutCtx, widget) {
+            editWidget(event, layoutCtx, widget);
+        },
+        onExportWidget: function(event, layoutCtx, widget) {
+            exportWidget(event, layoutCtx, widget);
+        },
+        onWidgetMouseDown: function(event, layoutCtx, widget) {
+            widgetMouseDown(event, layoutCtx, widget);
+        },
+        onWidgetClicked: function(event, layoutCtx, widget) {
+            widgetClicked(event, layoutCtx, widget);
+        },
+        prepareDashboardContextMenu: function(layoutCtx) {
+            return prepareDashboardContextMenu(layoutCtx);
+        },
+        prepareWidgetContextMenu: function(layoutCtx, widget) {
+            return prepareWidgetContextMenu(layoutCtx, widget);
+        },
+        onRemoveWidget: function(event, layoutCtx, widget) {
+            removeWidget(event, layoutCtx, widget);
+        },
+        copyWidget: function($event, layoutCtx, widget) {
+            copyWidget($event, layoutCtx, widget);
+        },
+        copyWidgetReference: function($event, layoutCtx, widget) {
+            copyWidgetReference($event, layoutCtx, widget);
+        },
+        pasteWidget: function($event, layoutCtx, pos) {
+            pasteWidget($event, layoutCtx, pos);
+        },
+        pasteWidgetReference: function($event, layoutCtx, pos) {
+            pasteWidgetReference($event, layoutCtx, pos);
+        }
+    };
+
     $scope.$watch('vm.widgetsBundle', function (newVal, prevVal) {
         if (newVal !== prevVal && !vm.widgetEditMode) {
             loadWidgetLibrary();
@@ -132,6 +255,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
         }
     });
 
+    loadDashboard();
 
     function loadWidgetLibrary() {
         vm.latestWidgetTypes = [];
@@ -199,34 +323,29 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     function loadDashboard() {
-
-        var deferred = $q.defer();
-
         if (vm.widgetEditMode) {
-            $timeout(function () {
-                vm.dashboardConfiguration = {
-                    timewindow: timeService.defaultTimewindow()
-                };
-                vm.widgets = [{
-                    isSystemType: true,
-                    bundleAlias: 'customWidgetBundle',
-                    typeAlias: 'customWidget',
-                    type: $rootScope.editWidgetInfo.type,
-                    title: 'My widget',
-                    sizeX: $rootScope.editWidgetInfo.sizeX * 2,
-                    sizeY: $rootScope.editWidgetInfo.sizeY * 2,
-                    row: 2,
-                    col: 4,
-                    config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig)
-                }];
-                vm.widgets[0].config.title = vm.widgets[0].config.title || $rootScope.editWidgetInfo.widgetName;
-                deferred.resolve();
-                var parentScope = $window.parent.angular.element($window.frameElement).scope();
-                parentScope.$root.$broadcast('widgetEditModeInited');
-                parentScope.$root.$apply();
-            });
+            var widget = {
+                isSystemType: true,
+                bundleAlias: 'customWidgetBundle',
+                typeAlias: 'customWidget',
+                type: $rootScope.editWidgetInfo.type,
+                title: 'My widget',
+                sizeX: $rootScope.editWidgetInfo.sizeX * 2,
+                sizeY: $rootScope.editWidgetInfo.sizeY * 2,
+                row: 2,
+                col: 4,
+                config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig)
+            };
+            widget.config.title = widget.config.title || $rootScope.editWidgetInfo.widgetName;
+
+            vm.dashboard = dashboardUtils.createSingleWidgetDashboard(widget);
+            vm.dashboardConfiguration = vm.dashboard.configuration;
+            vm.dashboardCtx.dashboard = vm.dashboard;
+            vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
+            var parentScope = $window.parent.angular.element($window.frameElement).scope();
+            parentScope.$root.$broadcast('widgetEditModeInited');
+            parentScope.$root.$apply();
         } else {
-
             dashboardService.getDashboard($stateParams.dashboardId)
                 .then(function success(dashboard) {
                     vm.dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
@@ -236,34 +355,68 @@ export default function DashboardController(types, dashboardUtils, widgetService
                                 if (resolution.error && !isTenantAdmin()) {
                                     vm.configurationError = true;
                                     showAliasesResolutionError(resolution.error);
-                                    deferred.reject();
                                 } else {
-                                    vm.aliasesInfo = resolution.aliasesInfo;
                                     vm.dashboardConfiguration = vm.dashboard.configuration;
-                                    vm.widgets = vm.dashboard.configuration.widgets;
-                                    deferred.resolve();
+                                    vm.dashboardCtx.dashboard = vm.dashboard;
+                                    vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo;
+                                    vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
                                 }
                             }
                         );
-                }, function fail(e) {
-                    deferred.reject(e);
+                }, function fail() {
+                    vm.configurationError = true;
                 });
-
         }
-        return deferred.promise;
     }
 
-    function dashboardInitFailed() {
-        var parentScope = $window.parent.angular.element($window.frameElement).scope();
-        parentScope.$emit('widgetEditModeInited');
-        parentScope.$apply();
-        vm.dashboardInitComplete = true;
+    function openDashboardState(state) {
+        var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, state);
+        if (layoutsData) {
+            vm.dashboardCtx.state = state;
+            var layoutVisibilityChanged = false;
+            for (var l in vm.layouts) {
+                var layout = vm.layouts[l];
+                var showLayout;
+                if (layoutsData[l]) {
+                    showLayout = true;
+                } else {
+                    showLayout = false;
+                }
+                if (layout.show != showLayout) {
+                    layout.show = showLayout;
+                    layoutVisibilityChanged = !vm.isMobile;
+                }
+            }
+            vm.isRightLayoutOpened = false;
+            updateLayouts(layoutVisibilityChanged);
+        }
+
+        function updateLayouts(layoutVisibilityChanged) {
+            for (l in vm.layouts) {
+                layout = vm.layouts[l];
+                if (layoutsData[l]) {
+                    var layoutInfo = layoutsData[l];
+                    if (layout.layoutCtx.id === 'main') {
+                        layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged);
+                    }
+                    updateLayout(layout, layoutInfo.widgets, layoutInfo.widgetLayouts, layoutInfo.gridSettings);
+                } else {
+                    updateLayout(layout, [], {}, null);
+                }
+            }
+        }
     }
 
-    function dashboardInited(dashboard) {
-        vm.dashboardContainer = dashboard;
-        initHotKeys();
-        vm.dashboardInitComplete = true;
+    function updateLayout(layout, widgets, widgetLayouts, gridSettings) {
+        if (gridSettings) {
+            layout.layoutCtx.gridSettings = gridSettings;
+        }
+        layout.layoutCtx.widgets = widgets;
+        layout.layoutCtx.widgetLayouts = widgetLayouts;
+        if (layout.show && layout.layoutCtx.ctrl) {
+            layout.layoutCtx.ctrl.reload();
+        }
+        layout.layoutCtx.ignoreLoading = true;
     }
 
     function isPublicUser() {
@@ -278,16 +431,12 @@ export default function DashboardController(types, dashboardUtils, widgetService
         return vm.user.authority === 'SYS_ADMIN';
     }
 
-    function noData() {
-        return vm.dashboardInitComplete && !vm.configurationError && vm.widgets.length == 0;
-    }
-
     function dashboardConfigurationError() {
-        return vm.dashboardInitComplete && vm.configurationError;
+        return vm.configurationError;
     }
 
     function showDashboardToolbar() {
-        return vm.dashboardInitComplete;
+        return true;
     }
 
     function openEntityAliases($event) {
@@ -298,7 +447,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
             locals: {
                 config: {
                     entityAliases: angular.copy(vm.dashboard.configuration.entityAliases),
-                    widgets: vm.widgets,
+                    widgets: dashboardUtils.getWidgetsArray(vm.dashboard),
                     isSingleEntityAlias: false,
                     singleEntityAlias: null
                 }
@@ -315,54 +464,115 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     function openDashboardSettings($event) {
+        var gridSettings = null;
+        var layoutKeys = dashboardUtils.isSingleLayoutDashboard(vm.dashboard);
+        if (layoutKeys) {
+            gridSettings = angular.copy(vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings)
+        }
         $mdDialog.show({
             controller: 'DashboardSettingsController',
             controllerAs: 'vm',
-            templateUrl: dashboardBackgroundTemplate,
+            templateUrl: dashboardSettingsTemplate,
             locals: {
-                gridSettings: angular.copy(vm.dashboard.configuration.gridSettings)
+                settings: angular.copy(vm.dashboard.configuration.settings),
+                gridSettings: gridSettings
             },
             parent: angular.element($document[0].body),
             skipHide: true,
             fullscreen: true,
             targetEvent: $event
-        }).then(function (gridSettings) {
-            var prevGridSettings = vm.dashboard.configuration.gridSettings;
-            var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
-            var ratio = gridSettings.columns / prevColumns;
-            var currentWidgets = angular.copy(vm.widgets);
-            vm.widgets = [];
-            vm.dashboard.configuration.gridSettings = gridSettings;
-            for (var w in currentWidgets) {
-                var widget = currentWidgets[w];
-                widget.sizeX = Math.round(widget.sizeX * ratio);
-                widget.sizeY = Math.round(widget.sizeY * ratio);
-                widget.col = Math.round(widget.col * ratio);
-                widget.row = Math.round(widget.row * ratio);
+        }).then(function (data) {
+            vm.dashboard.configuration.settings = data.settings;
+            var gridSettings = data.gridSettings;
+            if (gridSettings) {
+                updateLayoutGrid(layoutKeys, gridSettings);
             }
-            vm.dashboard.configuration.widgets = currentWidgets;
-            vm.widgets = vm.dashboard.configuration.widgets;
         }, function () {
         });
     }
 
-    function editWidget($event, widget) {
+    function manageDashboardLayouts($event) {
+        $mdDialog.show({
+            controller: 'ManageDashboardLayoutsController',
+            controllerAs: 'vm',
+            templateUrl: manageDashboardLayoutsTemplate,
+            locals: {
+                layouts: angular.copy(vm.dashboard.configuration.states[vm.dashboardCtx.state].layouts)
+            },
+            parent: angular.element($document[0].body),
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (layouts) {
+            updateLayouts(layouts);
+        }, function () {
+        });
+    }
+
+    function manageDashboardStates($event) {
+        var dashboardConfiguration = vm.dashboard.configuration;
+        var states = angular.copy(dashboardConfiguration.states);
+
+        $mdDialog.show({
+            controller: 'ManageDashboardStatesController',
+            controllerAs: 'vm',
+            templateUrl: manageDashboardStatesTemplate,
+            locals: {
+                states: states
+            },
+            parent: angular.element($document[0].body),
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (states) {
+            updateStates(states);
+        }, function () {
+        });
+    }
+
+    function updateLayoutGrid(layoutKeys, gridSettings) {
+        var layout = vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
+        var layoutCtx = vm.layouts[layoutKeys.layout];
+        layoutCtx.widgets = [];
+        dashboardUtils.updateLayoutSettings(layout, gridSettings);
+        var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, layoutKeys.state);
+        layoutCtx.widgets = layoutsData[layoutKeys.layout].widgets;
+    }
+
+    function updateLayouts(layouts) {
+        dashboardUtils.setLayouts(vm.dashboard, vm.dashboardCtx.state, layouts);
+        openDashboardState(vm.dashboardCtx.state);
+    }
+
+    function updateStates(states) {
+        vm.dashboard.configuration.states = states;
+        dashboardUtils.removeUnusedWidgets(vm.dashboard);
+        var targetState = vm.dashboardCtx.state;
+        if (!vm.dashboard.configuration.states[targetState]) {
+            targetState = dashboardUtils.getRootStateId(vm.dashboardConfiguration.states);
+        }
+        openDashboardState(targetState);
+    }
+
+    function editWidget($event, layoutCtx, widget) {
         $event.stopPropagation();
         if (vm.editingWidgetOriginal === widget) {
             $timeout(onEditWidgetClosed());
         } else {
             var transition = !vm.forceDashboardMobileMode;
             vm.editingWidgetOriginal = widget;
+            vm.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id];
             vm.editingWidget = angular.copy(vm.editingWidgetOriginal);
+            vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal);
+            vm.editingLayoutCtx = layoutCtx;
             vm.editingWidgetSubtitle = widgetService.getInstantWidgetInfo(vm.editingWidget).widgetName;
             vm.forceDashboardMobileMode = true;
             vm.isEditingWidget = true;
-
-            if (vm.dashboardContainer) {
+            if (layoutCtx) {
                 var delayOffset = transition ? 350 : 0;
                 var delay = transition ? 400 : 300;
                 $timeout(function () {
-                    vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, delay);
+                    layoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, delay);
                 }, delayOffset, false);
             }
         }
@@ -372,82 +582,36 @@ export default function DashboardController(types, dashboardUtils, widgetService
         importExport.exportDashboard(vm.currentDashboardId);
     }
 
-    function exportWidget($event, widget) {
+    function exportWidget($event, layoutCtx, widget) {
         $event.stopPropagation();
-        importExport.exportWidget(vm.dashboard, widget);
+        importExport.exportWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget);
     }
 
     function importWidget($event) {
         $event.stopPropagation();
-        importExport.importWidget($event, vm.dashboard, entityAliasesUpdated);
+        importExport.importWidget($event, vm.dashboard, vm.dashboardCtx.state,
+            selectTargetLayout, entityAliasesUpdated).then(
+            function success(importData) {
+                var widget = importData.widget;
+                var layoutId = importData.layoutId;
+                vm.layouts[layoutId].layoutCtx.widgets.push(widget);
+            }
+        );
     }
 
-    function widgetMouseDown($event, widget) {
+    function widgetMouseDown($event, layoutCtx, widget) {
         if (vm.isEdit && !vm.isEditingWidget) {
-            vm.dashboardContainer.selectWidget(widget, 0);
+            layoutCtx.ctrl.selectWidget(widget, 0);
         }
     }
 
-    function widgetClicked($event, widget) {
+    function widgetClicked($event, layoutCtx, widget) {
         if (vm.isEditingWidget) {
-            editWidget($event, widget);
+            editWidget($event, layoutCtx, widget);
         }
     }
 
-    function isHotKeyAllowed(event) {
-        var target = event.target || event.srcElement;
-        var scope = angular.element(target).scope();
-        return scope && scope.$parent !== $rootScope;
-    }
-
-    function initHotKeys() {
-        $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
-            hotkeys.bindTo($scope)
-                .add({
-                    combo: 'ctrl+c',
-                    description: translations['action.copy'],
-                    callback: function (event) {
-                        if (isHotKeyAllowed(event) &&
-                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
-                            var widget = vm.dashboardContainer.getSelectedWidget();
-                            if (widget) {
-                                event.preventDefault();
-                                copyWidget(event, widget);
-                            }
-                        }
-                    }
-                })
-                .add({
-                    combo: 'ctrl+v',
-                    description: translations['action.paste'],
-                    callback: function (event) {
-                        if (isHotKeyAllowed(event) &&
-                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
-                            if (itembuffer.hasWidget()) {
-                                event.preventDefault();
-                                pasteWidget(event);
-                            }
-                        }
-                    }
-                })
-                .add({
-                    combo: 'ctrl+x',
-                    description: translations['action.delete'],
-                    callback: function (event) {
-                        if (isHotKeyAllowed(event) &&
-                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
-                            var widget = vm.dashboardContainer.getSelectedWidget();
-                            if (widget) {
-                                event.preventDefault();
-                                removeWidget(event, widget);
-                            }
-                        }
-                    }
-                });
-        });
-    }
-
-    function prepareDashboardContextMenu() {
+    function prepareDashboardContextMenu(layoutCtx) {
         var dashboardContextActions = [];
         if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
             dashboardContextActions.push(
@@ -468,28 +632,39 @@ export default function DashboardController(types, dashboardUtils, widgetService
             );
             dashboardContextActions.push(
                 {
-                    action: pasteWidget,
+                    action: function ($event) {
+                        layoutCtx.ctrl.pasteWidget($event);
+                    },
                     enabled: itembuffer.hasWidget(),
                     value: "action.paste",
                     icon: "content_paste",
                     shortcut: "M-V"
                 }
             );
+            dashboardContextActions.push(
+                {
+                    action: function ($event) {
+                        layoutCtx.ctrl.pasteWidgetReference($event);
+                    },
+                    enabled: itembuffer.canPasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id),
+                    value: "action.paste-reference",
+                    icon: "content_paste",
+                    shortcut: "M-I"
+                }
+            );
+
         }
         return dashboardContextActions;
     }
 
-    function pasteWidget($event) {
-        var pos = vm.dashboardContainer.getEventGridPosition($event);
-        itembuffer.pasteWidget(vm.dashboard, pos, entityAliasesUpdated);
-    }
-
-    function prepareWidgetContextMenu() {
+    function prepareWidgetContextMenu(layoutCtx) {
         var widgetContextActions = [];
         if (vm.isEdit && !vm.isEditingWidget) {
             widgetContextActions.push(
                 {
-                    action: editWidget,
+                    action: function (event, widget) {
+                        editWidget(event, layoutCtx, widget);
+                    },
                     enabled: true,
                     value: "action.edit",
                     icon: "edit"
@@ -498,7 +673,9 @@ export default function DashboardController(types, dashboardUtils, widgetService
             if (!vm.widgetEditMode) {
                 widgetContextActions.push(
                     {
-                        action: copyWidget,
+                        action: function (event, widget) {
+                            copyWidget(event, layoutCtx, widget);
+                        },
                         enabled: true,
                         value: "action.copy",
                         icon: "content_copy",
@@ -507,7 +684,20 @@ export default function DashboardController(types, dashboardUtils, widgetService
                 );
                 widgetContextActions.push(
                     {
-                        action: removeWidget,
+                        action: function (event, widget) {
+                            copyWidgetReference(event, layoutCtx, widget);
+                        },
+                        enabled: true,
+                        value: "action.copy-reference",
+                        icon: "content_copy",
+                        shortcut: "M-R"
+                    }
+                );
+                widgetContextActions.push(
+                    {
+                        action: function (event, widget) {
+                            removeWidget(event, layoutCtx, widget);
+                        },
                         enabled: true,
                         value: "action.delete",
                         icon: "clear",
@@ -519,8 +709,12 @@ export default function DashboardController(types, dashboardUtils, widgetService
         return widgetContextActions;
     }
 
-    function copyWidget($event, widget) {
-        itembuffer.copyWidget(vm.dashboard, widget);
+    function copyWidget($event, layoutCtx, widget) {
+        itembuffer.copyWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget);
+    }
+
+    function copyWidgetReference($event, layoutCtx, widget) {
+        itembuffer.copyWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget);
     }
 
     function helpLinkIdForWidgetType() {
@@ -548,37 +742,55 @@ export default function DashboardController(types, dashboardUtils, widgetService
         return link;
     }
 
+    function toolbarAlwaysOpen() {
+        if (vm.dashboard && vm.dashboard.configuration.settings &&
+            angular.isDefined(vm.dashboard.configuration.settings.toolbarAlwaysOpen)) {
+            return vm.dashboard.configuration.settings.toolbarAlwaysOpen;
+        } else {
+            return false;
+        }
+    }
+
     function displayTitle() {
-        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
-            angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) {
-            return vm.dashboard.configuration.gridSettings.showTitle;
+        if (vm.dashboard && vm.dashboard.configuration.settings &&
+            angular.isDefined(vm.dashboard.configuration.settings.showTitle)) {
+            return vm.dashboard.configuration.settings.showTitle;
         } else {
             return true;
         }
     }
 
     function displayExport() {
-        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
-            angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardExport)) {
-            return vm.dashboard.configuration.gridSettings.showDashboardExport;
+        if (vm.dashboard && vm.dashboard.configuration.settings &&
+            angular.isDefined(vm.dashboard.configuration.settings.showDashboardExport)) {
+            return vm.dashboard.configuration.settings.showDashboardExport;
         } else {
             return true;
         }
     }
 
     function displayDashboardTimewindow() {
-        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
-            angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardTimewindow)) {
-            return vm.dashboard.configuration.gridSettings.showDashboardTimewindow;
+        if (vm.dashboard && vm.dashboard.configuration.settings &&
+            angular.isDefined(vm.dashboard.configuration.settings.showDashboardTimewindow)) {
+            return vm.dashboard.configuration.settings.showDashboardTimewindow;
+        } else {
+            return true;
+        }
+    }
+
+    function displayDashboardsSelect() {
+        if (vm.dashboard && vm.dashboard.configuration.settings &&
+            angular.isDefined(vm.dashboard.configuration.settings.showDashboardsSelect)) {
+            return vm.dashboard.configuration.settings.showDashboardsSelect;
         } else {
             return true;
         }
     }
 
     function displayEntitiesSelect() {
-        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
-            angular.isDefined(vm.dashboard.configuration.gridSettings.showEntitiesSelect)) {
-            return vm.dashboard.configuration.gridSettings.showEntitiesSelect;
+        if (vm.dashboard && vm.dashboard.configuration.settings &&
+            angular.isDefined(vm.dashboard.configuration.settings.showEntitiesSelect)) {
+            return vm.dashboard.configuration.settings.showEntitiesSelect;
         } else {
             return true;
         }
@@ -588,32 +800,40 @@ export default function DashboardController(types, dashboardUtils, widgetService
         if (widgetForm.$dirty) {
             widgetForm.$setPristine();
             vm.editingWidget = angular.copy(vm.editingWidgetOriginal);
+            vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal);
         }
     }
 
     function saveWidget(widgetForm) {
         widgetForm.$setPristine();
         var widget = angular.copy(vm.editingWidget);
-        var index = vm.widgets.indexOf(vm.editingWidgetOriginal);
-        vm.widgets[index] = widget;
+        var widgetLayout = angular.copy(vm.editingWidgetLayout);
+        var id = vm.editingWidgetOriginal.id;
+        var index = vm.editingLayoutCtx.widgets.indexOf(vm.editingWidgetOriginal);
+        vm.dashboardConfiguration.widgets[id] = widget;
         vm.editingWidgetOriginal = widget;
-        vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, 0);
+        vm.editingWidgetLayoutOriginal = widgetLayout;
+        vm.editingLayoutCtx.widgets[index] = widget;
+        vm.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout;
+        vm.editingLayoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, 0);
     }
 
     function onEditWidgetClosed() {
         vm.editingWidgetOriginal = null;
         vm.editingWidget = null;
+        vm.editingWidgetLayoutOriginal = null;
+        vm.editingWidgetLayout = null;
+        vm.editingLayoutCtx = null;
         vm.editingWidgetSubtitle = null;
         vm.isEditingWidget = false;
-        if (vm.dashboardContainer) {
-            vm.dashboardContainer.resetHighlight();
-        }
+        resetHighlight();
         vm.forceDashboardMobileMode = false;
     }
 
-    function addWidget() {
+    function addWidget(event, layoutCtx) {
         loadWidgetLibrary();
         vm.isAddingWidget = true;
+        vm.addingLayoutCtx = layoutCtx;
     }
 
     function onAddWidgetClosed() {
@@ -623,6 +843,33 @@ export default function DashboardController(types, dashboardUtils, widgetService
         vm.staticWidgetTypes = [];
     }
 
+    function selectTargetLayout($event) {
+        var deferred = $q.defer();
+        var layouts = vm.dashboardConfiguration.states[vm.dashboardCtx.state].layouts;
+        var layoutIds = Object.keys(layouts);
+        if (layoutIds.length > 1) {
+            $mdDialog.show({
+                controller: 'SelectTargetLayoutController',
+                controllerAs: 'vm',
+                templateUrl: selectTargetLayoutTemplate,
+                parent: angular.element($document[0].body),
+                fullscreen: true,
+                skipHide: true,
+                targetEvent: $event
+            }).then(
+                function success(layoutId) {
+                    deferred.resolve(layoutId);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        } else {
+            deferred.resolve(layoutIds[0]);
+        }
+        return deferred.promise;
+    }
+
     function addWidgetFromType(event, widget) {
         vm.onAddWidgetClosed();
         vm.isAddingWidget = false;
@@ -642,17 +889,22 @@ export default function DashboardController(types, dashboardUtils, widgetService
                     config: config
                 };
 
+                function addWidgetToLayout(widget, layoutId) {
+                    dashboardUtils.addWidgetToLayout(vm.dashboard, vm.dashboardCtx.state, layoutId, widget);
+                    vm.layouts[layoutId].layoutCtx.widgets.push(widget);
+                }
+
                 function addWidget(widget) {
-                    var columns = 24;
-                    if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
-                        columns = vm.dashboard.configuration.gridSettings.columns;
-                    }
-                    if (columns != 24) {
-                        var ratio = columns / 24;
-                        widget.sizeX *= ratio;
-                        widget.sizeY *= ratio;
+                    if (vm.addingLayoutCtx) {
+                        addWidgetToLayout(widget, vm.addingLayoutCtx.id);
+                        vm.addingLayoutCtx = null;
+                    } else {
+                        selectTargetLayout(event).then(
+                            function success(layoutId) {
+                                addWidgetToLayout(widget, layoutId);
+                            }
+                        );
                     }
-                    vm.widgets.push(widget);
                 }
 
                 if (widgetTypeInfo.useCustomDatasources) {
@@ -664,7 +916,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
                         templateUrl: addWidgetTemplate,
                         locals: {
                             dashboard: vm.dashboard,
-                            aliasesInfo: vm.aliasesInfo,
+                            aliasesInfo: vm.dashboardCtx.aliasesInfo,
                             widget: newWidget,
                             widgetInfo: widgetTypeInfo
                         },
@@ -678,17 +930,17 @@ export default function DashboardController(types, dashboardUtils, widgetService
                         }
                     }).then(function (result) {
                         var widget = result.widget;
-                        vm.aliasesInfo = result.aliasesInfo;
+                        vm.dashboardCtx.aliasesInfo = result.aliasesInfo;
                         addWidget(widget);
                     }, function (rejection) {
-                        vm.aliasesInfo = rejection.aliasesInfo;
+                        vm.dashboardCtx.aliasesInfo = rejection.aliasesInfo;
                     });
                 }
             }
         );
     }
 
-    function removeWidget(event, widget) {
+    function removeWidget(event, layoutCtx, widget) {
         var title = widget.config.title;
         if (!title || title.length === 0) {
             title = widgetService.getInstantWidgetInfo(widget).widgetName;
@@ -701,32 +953,64 @@ export default function DashboardController(types, dashboardUtils, widgetService
             .cancel($translate.instant('action.no'))
             .ok($translate.instant('action.yes'));
         $mdDialog.show(confirm).then(function () {
-            vm.widgets.splice(vm.widgets.indexOf(widget), 1);
+            var index = layoutCtx.widgets.indexOf(widget);
+            if (index > -1) {
+                layoutCtx.widgets.splice(index, 1);
+                dashboardUtils.removeWidgetFromLayout(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget.id);
+            }
         });
     }
 
+    function pasteWidget(event, layoutCtx, pos) {
+        itembuffer.pasteWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos, entityAliasesUpdated).then(
+            function (widget) {
+                if (widget) {
+                    layoutCtx.widgets.push(widget);
+                }
+            }
+        );
+    }
+
+    function pasteWidgetReference(event, layoutCtx, pos) {
+        itembuffer.pasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos).then(
+            function (widget) {
+                if (widget) {
+                    layoutCtx.widgets.push(widget);
+                }
+            }
+        );
+    }
+
     function setEditMode(isEdit, revert) {
         vm.isEdit = isEdit;
         if (vm.isEdit) {
-            if (vm.widgetEditMode) {
-                vm.prevWidgets = angular.copy(vm.widgets);
-            } else {
-                vm.prevDashboard = angular.copy(vm.dashboard);
-            }
+            vm.prevDashboard = angular.copy(vm.dashboard);
+            vm.prevDashboardState = vm.dashboardCtx.state;
         } else {
             if (vm.widgetEditMode) {
                 if (revert) {
-                    vm.widgets = vm.prevWidgets;
+                    vm.dashboard = vm.prevDashboard;
                 }
             } else {
-                if (vm.dashboardContainer) {
-                    vm.dashboardContainer.resetHighlight();
-                }
+                resetHighlight();
                 if (revert) {
                     vm.dashboard = vm.prevDashboard;
-                    vm.widgets = vm.dashboard.configuration.widgets;
                     vm.dashboardConfiguration = vm.dashboard.configuration;
+                    vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
+                    openDashboardState(vm.prevDashboardState);
                     entityAliasesUpdated();
+                } else {
+                    vm.dashboard.configuration.timewindow = vm.dashboardCtx.dashboardTimewindow;
+                }
+            }
+        }
+    }
+
+    function resetHighlight() {
+        for (var l in vm.layouts) {
+            if (vm.layouts[l].layoutCtx) {
+                if (vm.layouts[l].layoutCtx.ctrl) {
+                    vm.layouts[l].layoutCtx.ctrl.resetHighlight();
                 }
             }
         }
@@ -756,20 +1040,26 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     function entityAliasesUpdated() {
+        var deferred = $q.defer();
         entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
             .then(
                 function(resolution) {
                     if (resolution.aliasesInfo) {
-                        vm.aliasesInfo = resolution.aliasesInfo;
+                        vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo;
                     }
+                   deferred.resolve();
                 }
             );
+        return deferred.promise;
     }
 
     function notifyDashboardUpdated() {
         if (vm.widgetEditMode) {
             var parentScope = $window.parent.angular.element($window.frameElement).scope();
-            var widget = vm.widgets[0];
+            var widget = vm.layouts.main.layoutCtx.widgets[0];
+            var layout = vm.layouts.main.layoutCtx.widgetLayouts[widget.id];
+            widget.sizeX = layout.sizeX;
+            widget.sizeY = layout.sizeY;
             parentScope.$root.$broadcast('widgetEditUpdated', widget);
             parentScope.$root.$apply();
         } else {
diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js
index e9fe1f2..92bb362 100644
--- a/ui/src/app/dashboard/dashboard.routes.js
+++ b/ui/src/app/dashboard/dashboard.routes.js
@@ -66,7 +66,8 @@ export default function DashboardRoutes($stateProvider) {
             }
         })
         .state('home.dashboards.dashboard', {
-            url: '/:dashboardId',
+            url: '/:dashboardId?state',
+            reloadOnSearch: false,
             module: 'private',
             auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
             views: {
@@ -86,7 +87,8 @@ export default function DashboardRoutes($stateProvider) {
             }
         })
         .state('home.customers.dashboards.dashboard', {
-            url: '/:dashboardId',
+            url: '/:dashboardId?state',
+            reloadOnSearch: false,
             module: 'private',
             auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
             views: {
diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss
index 8f50ca2..ca2bbab 100644
--- a/ui/src/app/dashboard/dashboard.scss
+++ b/ui/src/app/dashboard/dashboard.scss
@@ -63,76 +63,46 @@ tb-details-sidenav.tb-widget-details-sidenav {
 section.tb-dashboard-toolbar {
   position: absolute;
   top: 0px;
-  left: -100%;
+  left: 0px;
   z-index: 3;
   pointer-events: none;
   &.tb-dashboard-toolbar-opened {
     right: 0px;
-    @include transition(right .3s cubic-bezier(.55,0,.55,.2));
+   // @include transition(right .3s cubic-bezier(.55,0,.55,.2));
   }
   &.tb-dashboard-toolbar-closed {
     right: 18px;
     @include transition(right .3s cubic-bezier(.55,0,.55,.2) .2s);
   }
-  md-fab-toolbar {
-    &.md-is-open {
-      md-fab-trigger {
-        .md-button {
-          &.md-fab {
-            opacity: 1;
-            @include transition(opacity .3s cubic-bezier(.55,0,.55,.2));
-          }
-        }
-      }
-    }
-    md-fab-trigger {
-      .md-button {
-        &.md-fab {
-          line-height: 36px;
-          width: 36px;
-          height: 36px;
-          margin: 4px 0 0 4px;
-          opacity: 0.5;
-          @include transition(opacity .3s cubic-bezier(.55,0,.55,.2) .2s);
-          md-icon {
-            position: absolute;
-            top: 25%;
-            margin: 0;
-            line-height: 18px;
-            height: 18px;
-            width: 18px;
-            min-height: 18px;
-            min-width: 18px;
-          }
-        }
-      }
-    }
-    .md-fab-toolbar-wrapper {
-      height: 50px;
-      md-toolbar {
-        min-height: 46px;
-        height: 46px;
-        md-fab-actions {
-          font-size: 16px;
-          margin-top: 0px;
-          .close-action {
-            margin-right: -18px;
-          }
-        }
-      }
-    }
-  }
 }
 
 .tb-dashboard-container {
    &.tb-dashboard-toolbar-opened {
-     margin-top: 50px;
-     @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2));
+     &.is-fullscreen {
+       margin-top: 64px;
+     }
+     &:not(.is-fullscreen) {
+       margin-top: 50px;
+       @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2));
+     }
    }
    &.tb-dashboard-toolbar-closed {
      margin-top: 0px;
      @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2) .2s);
   }
+  .tb-dashboard-layouts {
+    md-backdrop {
+      z-index: 1;
+    }
+    #tb-main-layout {
+
+    }
+    #tb-right-layout {
+      md-sidenav {
+        z-index: 1;
+      }
+    }
+  }
 }
 
 /*****************************
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 3286f8d..8338612 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -16,33 +16,25 @@
 
 -->
 <md-content flex tb-expand-fullscreen="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-button-id="dashboard-expand-button"
-            hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom"
-            ng-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
-                    'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
-                    'background-repeat': 'no-repeat',
-                    'background-attachment': 'scroll',
-                    'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
-                    'background-position': '0% 0%'}">
+            hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom">
     <section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
              ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
-        <md-fab-toolbar md-open="vm.toolbarOpened"
-                        md-direction="left">
-            <md-fab-trigger class="align-with-text">
-                <md-button aria-label="menu" class="md-fab md-primary" ng-click="vm.openToolbar()">
-                    <md-tooltip ng-show="!vm.toolbarOpened" md-direction="bottom">
-                        {{ 'dashboard.open-toolbar' | translate }}
-                    </md-tooltip>
-                    <md-icon aria-label="dashboard-toolbar" class="material-icons">more_horiz</md-icon>
-                </md-button>
-            </md-fab-trigger>
-            <md-toolbar>
-                <md-fab-actions class="md-toolbar-tools">
-                    <md-button ng-show="!vm.isEdit" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
+        <tb-dashboard-toolbar ng-show="!vm.widgetEditMode" force-fullscreen="forceFullscreen"
+                              toolbar-opened="vm.toolbarOpened" on-trigger-click="vm.openToolbar()">
+            <div class="tb-dashboard-action-panels" flex layout="row" layout-align="start center">
+                <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="start center">
+                    <md-button ng-show="vm.showCloseToolbar()" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
                         <md-tooltip md-direction="bottom">
                             {{ 'dashboard.close-toolbar' | translate }}
                         </md-tooltip>
                         <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
                     </md-button>
+                    <md-button ng-show="vm.showRightLayoutSwitch()" aria-label="switch-layouts" class="md-icon-button" ng-click="vm.toggleLayouts()">
+                        <ng-md-icon icon="{{vm.isRightLayoutOpened ? 'arrow_back' : 'menu'}}" options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+                        <md-tooltip md-direction="bottom">
+                            {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}
+                        </md-tooltip>
+                    </md-button>
                     <md-button id="dashboard-expand-button"
                                aria-label="{{ 'fullscreen.fullscreen' | translate }}"
                                class="md-icon-button">
@@ -61,12 +53,12 @@
                                    is-toolbar
                                    direction="left"
                                    tooltip-direction="bottom" aggregation
-                                   ng-model="vm.dashboardConfiguration.timewindow">
+                                   ng-model="vm.dashboardCtx.dashboardTimewindow">
                     </tb-timewindow>
                     <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
                                               tooltip-direction="bottom"
-                                              ng-model="vm.aliasesInfo.entityAliases"
-                                              entity-aliases-info="vm.aliasesInfo.entityAliasesInfo">
+                                              ng-model="vm.dashboardCtx.aliasesInfo.entityAliases"
+                                              entity-aliases-info="vm.dashboardCtx.aliasesInfo.entityAliasesInfo">
                     </tb-aliases-entity-select>
                     <md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
                                ng-click="vm.openEntityAliases($event)">
@@ -82,32 +74,45 @@
                         </md-tooltip>
                         <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
                     </md-button>
-                    <tb-dashboard-select   ng-show="!vm.isEdit && !vm.widgetEditMode"
+                    <tb-dashboard-select   ng-show="!vm.isEdit && !vm.widgetEditMode && vm.displayDashboardsSelect()"
                                            ng-model="vm.currentDashboardId"
                                            dashboards-scope="{{vm.currentDashboardScope}}"
                                            customer-id="vm.currentCustomerId">
                     </tb-dashboard-select>
-                </md-fab-actions>
-            </md-toolbar>
-        </md-fab-toolbar>
+                </div>
+                <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="end center">
+                    <div layout="row" layout-align="start center" ng-show="vm.isEdit">
+                        <md-button aria-label="{{ 'dashboard.manage-states' | translate }}" class="md-icon-button"
+                                   ng-click="vm.manageDashboardStates($event)">
+                            <md-tooltip md-direction="bottom">
+                                {{ 'dashboard.manage-states' | translate }}
+                            </md-tooltip>
+                            <md-icon aria-label="{{ 'dashboard.manage-states' | translate }}" class="material-icons">layers</md-icon>
+                        </md-button>
+                        <md-button aria-label="{{ 'layout.manage' | translate }}" class="md-icon-button"
+                                   ng-click="vm.manageDashboardLayouts($event)">
+                            <md-tooltip md-direction="bottom">
+                                {{ 'layout.manage' | translate }}
+                            </md-tooltip>
+                            <md-icon aria-label="{{ 'layout.manage' | translate }}" class="material-icons">view_compact</md-icon>
+                        </md-button>
+                    </div>
+                    <div layout="row" layout-align="start center">
+                        <tb-states-component ng-if="vm.isEdit" states-controller-id="'default'"
+                                             dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+                        </tb-states-component>
+                        <tb-states-component ng-if="!vm.isEdit" states-controller-id="vm.dashboardConfiguration.settings.stateControllerId"
+                                             dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+                        </tb-states-component>
+                    </div>
+                 </div>
+            </div>
+        </tb-dashboard-toolbar>
     </section>
     <section class="tb-dashboard-container tb-absolute-fill"
-             ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
-        <section ng-show="!loading && vm.noData()" layout-align="center center"
-                 ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
-                 ng-class="{'tb-padded' : !vm.widgetEditMode}"
-                 style="text-transform: uppercase; display: flex; z-index: 1;"
-                 class="md-headline tb-absolute-fill">
-            <span translate ng-if="!vm.isEdit">
-                dashboard.no-widgets
-            </span>
-            <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)">
-                <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
-                {{ 'dashboard.add-widget' | translate }}
-            </md-button>
-        </section>
+             ng-class="{ 'is-fullscreen': forceFullscreen, 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
         <section ng-show="!loading && vm.dashboardConfigurationError()" layout-align="center center"
-                 ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
+                 ng-style="{'color': vm.dashboard.configuration.settings.titleColor}"
                  ng-class="{'tb-padded' : !vm.widgetEditMode}"
                  style="text-transform: uppercase; display: flex; z-index: 1;"
                  class="md-headline tb-absolute-fill">
@@ -116,46 +121,47 @@
             </span>
         </section>
         <section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center"
-                 ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
+                 ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">
             <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
             <md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
-                <label translate ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">dashboard.title</label>
-                <input class="tb-dashboard-title" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}" required name="title" ng-model="vm.dashboard.title">
+                <label translate ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">dashboard.title</label>
+                <input class="tb-dashboard-title" ng-style="{'color': vm.dashboard.configuration.settings.titleColor}" required name="title" ng-model="vm.dashboard.title">
             </md-input-container>
         </section>
-        <div class="tb-absolute-fill"
+        <div class="tb-absolute-fill tb-dashboard-layouts" layout="{{vm.forceDashboardMobileMode ? 'column' : 'row'}}"
              ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">
-            <tb-dashboard
-                    dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
-                        'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
-                        'background-repeat': 'no-repeat',
-                        'background-attachment': 'scroll',
-                        'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
-                        'background-position': '0% 0%'}"
-                    widgets="vm.widgets"
-                    columns="vm.dashboard.configuration.gridSettings.columns"
-                    margins="vm.dashboard.configuration.gridSettings.margins"
-                    aliases-info="vm.aliasesInfo"
-                    dashboard-timewindow="vm.dashboardConfiguration.timewindow"
-                    is-edit="vm.isEdit"
-                    is-mobile="vm.forceDashboardMobileMode"
-                    is-mobile-disabled="vm.widgetEditMode"
-                    is-edit-action-enabled="vm.isEdit"
-                    is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
-                    is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
-                    on-edit-widget="vm.editWidget(event, widget)"
-                    on-export-widget="vm.exportWidget(event, widget)"
-                    on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
-                    on-widget-clicked="vm.widgetClicked(event, widget)"
-                    on-widget-context-menu="vm.widgetContextMenu(event, widget)"
-                    prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
-                    prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
-                    on-remove-widget="vm.removeWidget(event, widget)"
-                    load-widgets="vm.loadDashboard()"
-                    get-st-diff="vm.getServerTimeDiff()"
-                    on-init="vm.dashboardInited(dashboard)"
-                    on-init-failed="vm.dashboardInitFailed(e)">
-            </tb-dashboard>
+            <div ng-show="vm.layouts.main.show"
+                 id="tb-main-layout"
+                 ng-style="{width: vm.mainLayoutWidth(),
+                            height: vm.mainLayoutHeight()}">
+                <tb-dashboard-layout layout-ctx="vm.layouts.main.layoutCtx"
+                                     dashboard-ctx="vm.dashboardCtx"
+                                     is-edit="vm.isEdit"
+                                     is-mobile="vm.forceDashboardMobileMode"
+                                     widget-edit-mode="vm.widgetEditMode"
+                                     get-st-diff="vm.getServerTimeDiff()">
+                </tb-dashboard-layout>
+            </div>
+            <md-sidenav ng-if="vm.layouts.right.show"
+                        id="tb-right-layout"
+                        class="md-sidenav-right"
+                        ng-style="{minWidth: vm.rightLayoutWidth(),
+                                   maxWidth: vm.rightLayoutWidth(),
+                                   height: vm.rightLayoutHeight(),
+                                   zIndex: 1}"
+                        md-component-id="right-dashboard-layout"
+                        aria-label="Right dashboard layout"
+                        md-is-open="!vm.isMobile || vm.isRightLayoutOpened"
+                        md-is-locked-open="!vm.isMobile">
+                <tb-dashboard-layout style="height: 100%;"
+                                     layout-ctx="vm.layouts.right.layoutCtx"
+                                     dashboard-ctx="vm.dashboardCtx"
+                                     is-edit="vm.isEdit"
+                                     is-mobile="vm.forceDashboardMobileMode"
+                                     widget-edit-mode="vm.widgetEditMode"
+                                     get-st-diff="vm.getServerTimeDiff()">
+                </tb-dashboard-layout>
+            </md-sidenav>
         </div>
         <tb-details-sidenav class="tb-widget-details-sidenav"
                             header-title="{{vm.editingWidget.config.title}}"
@@ -173,8 +179,9 @@
             <form name="vm.widgetForm" ng-if="vm.isEditingWidget">
                 <tb-edit-widget
                         dashboard="vm.dashboard"
-                        aliases-info="vm.aliasesInfo"
+                        aliases-info="vm.dashboardCtx.aliasesInfo"
                         widget="vm.editingWidget"
+                        widget-layout="vm.editingWidgetLayout"
                         the-form="vm.widgetForm">
                 </tb-edit-widget>
             </form>
@@ -286,7 +293,7 @@
                     </md-button>
                 </md-fab-actions>
             </md-fab-speed-dial>
-            <md-button ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"
+            <md-button ng-if="(vm.isTenantAdmin() || vm.isSystemAdmin()) && !forceFullscreen" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"
                        class="tb-btn-footer md-accent md-hue-2 md-fab"
                        aria-label="{{ 'action.apply' | translate }}"
                        ng-click="vm.saveDashboard()">
@@ -296,7 +303,7 @@
                 <ng-md-icon icon="done"></ng-md-icon>
             </md-button>
             <md-button ng-show="!vm.isAddingWidget && !loading"
-                       ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-disabled="loading"
+                       ng-if="(vm.isTenantAdmin() || vm.isSystemAdmin()) && !forceFullscreen" ng-disabled="loading"
                        class="tb-btn-footer md-accent md-hue-2 md-fab"
                        aria-label="{{ 'action.edit-mode' | translate }}"
                        ng-click="vm.toggleDashboardEditMode()">
@@ -308,7 +315,7 @@
             </md-button>
         </section>
     </section>
-    <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
+    <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">
         <span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ vm.thingsboardVersion }}</a></span>
     </section>
 </md-content>
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index ee107cc..ac98709 100644
--- a/ui/src/app/dashboard/dashboard-settings.controller.js
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -16,7 +16,7 @@
 import './dashboard-settings.scss';
 
 /*@ngInject*/
-export default function DashboardSettingsController($scope, $mdDialog, gridSettings) {
+export default function DashboardSettingsController($scope, $mdDialog, statesControllerService, settings, gridSettings) {
 
     var vm = this;
 
@@ -25,32 +25,53 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
     vm.imageAdded = imageAdded;
     vm.clearImage = clearImage;
 
-    vm.gridSettings = gridSettings || {};
+    vm.settings = settings;
+    vm.gridSettings = gridSettings;
+    vm.stateControllers = statesControllerService.getStateControllers();
 
-    if (angular.isUndefined(vm.gridSettings.showTitle)) {
-        vm.gridSettings.showTitle = true;
-    }
+    if (vm.settings) {
+        if (angular.isUndefined(vm.settings.stateControllerId)) {
+            vm.settings.stateControllerId = 'default';
+        }
 
-    if (angular.isUndefined(vm.gridSettings.showEntitiesSelect)) {
-        vm.gridSettings.showEntitiesSelect = true;
-    }
+        if (angular.isUndefined(vm.settings.showTitle)) {
+            vm.settings.showTitle = true;
+        }
 
-    if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) {
-        vm.gridSettings.showDashboardTimewindow = true;
-    }
+        if (angular.isUndefined(vm.settings.titleColor)) {
+            vm.settings.titleColor = 'rgba(0,0,0,0.870588)';
+        }
 
-    if (angular.isUndefined(vm.gridSettings.showDashboardExport)) {
-        vm.gridSettings.showDashboardExport = true;
-    }
+        if (angular.isUndefined(vm.settings.showDashboardsSelect)) {
+            vm.settings.showDashboardsSelect = true;
+        }
+
+        if (angular.isUndefined(vm.settings.showEntitiesSelect)) {
+            vm.settings.showEntitiesSelect = true;
+        }
 
-    vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
-    vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)';
-    vm.gridSettings.columns = vm.gridSettings.columns || 24;
-    vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
-    vm.hMargin = vm.gridSettings.margins[0];
-    vm.vMargin = vm.gridSettings.margins[1];
+        if (angular.isUndefined(vm.settings.showDashboardTimewindow)) {
+            vm.settings.showDashboardTimewindow = true;
+        }
 
-    vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%';
+        if (angular.isUndefined(vm.settings.showDashboardExport)) {
+            vm.settings.showDashboardExport = true;
+        }
+
+        if (angular.isUndefined(vm.settings.toolbarAlwaysOpen)) {
+            vm.settings.toolbarAlwaysOpen = false;
+        }
+    }
+
+    if (vm.gridSettings) {
+        vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
+        vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)';
+        vm.gridSettings.columns = vm.gridSettings.columns || 24;
+        vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
+        vm.hMargin = vm.gridSettings.margins[0];
+        vm.vMargin = vm.gridSettings.margins[1];
+        vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%';
+    }
 
     function cancel() {
         $mdDialog.cancel();
@@ -76,7 +97,14 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
 
     function save() {
         $scope.theForm.$setPristine();
-        vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
-        $mdDialog.hide(vm.gridSettings);
+        if (vm.gridSettings) {
+            vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
+        }
+        $mdDialog.hide(
+            {
+                settings: vm.settings,
+                gridSettings: vm.gridSettings
+            }
+        );
     }
 }
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
index 6ae4746..f3ff704 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -19,7 +19,7 @@
     <form name="theForm" ng-submit="vm.save()">
         <md-toolbar>
             <div class="md-toolbar-tools">
-                <h2 translate>dashboard.settings</h2>
+                <h2 translate>{{vm.settings ? 'dashboard.settings' : 'layout.settings'}}</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>
@@ -31,15 +31,56 @@
         <md-dialog-content>
             <div class="md-dialog-content">
                 <fieldset ng-disabled="loading">
-                    <div layout="row" layout-align="start center">
-                        <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
-                                     ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }}
-                        </md-checkbox>
+                    <div ng-show="vm.settings">
+                       <md-input-container class="md-block">
+                            <label translate>dashboard.state-controller</label>
+                            <md-select aria-label="{{ 'dashboard.state-controller' | translate }}" ng-model="vm.settings.stateControllerId">
+                                <md-option ng-repeat="(stateControllerId, stateController) in vm.stateControllers" ng-value="stateControllerId">
+                                    {{stateControllerId}}
+                                </md-option>
+                            </md-select>
+                        </md-input-container>
+                        <div layout="row" layout-align="start center">
+                            <md-checkbox flex aria-label="{{ 'dashboard.toolbar-always-open' | translate }}"
+                                         ng-model="vm.settings.toolbarAlwaysOpen">{{ 'dashboard.toolbar-always-open' | translate }}
+                            </md-checkbox>
+                            <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
+                                         ng-model="vm.settings.showTitle">{{ 'dashboard.display-title' | translate }}
+                            </md-checkbox>
+                            <div flex
+                                 ng-required="false"
+                                 md-color-picker
+                                 ng-model="vm.settings.titleColor"
+                                 label="{{ 'dashboard.title-color' | translate }}"
+                                 icon="format_color_fill"
+                                 default="rgba(0, 0, 0, 0.870588)"
+                                 md-color-clear-button="false"
+                                 open-on-input="true"
+                                 md-color-generic-palette="false"
+                                 md-color-history="false"
+                            ></div>
+                        </div>
+                        <div layout="row" layout-align="start center">
+                            <md-checkbox flex aria-label="{{ 'dashboard.display-dashboards-selection' | translate }}"
+                                         ng-model="vm.settings.showDashboardsSelect">{{ 'dashboard.display-dashboards-selection' | translate }}
+                            </md-checkbox>
+                            <md-checkbox flex aria-label="{{ 'dashboard.display-entities-selection' | translate }}"
+                                         ng-model="vm.settings.showEntitiesSelect">{{ 'dashboard.display-entities-selection' | translate }}
+                            </md-checkbox>
+                            <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
+                                         ng-model="vm.settings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
+                            </md-checkbox>
+                            <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
+                                         ng-model="vm.settings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
+                            </md-checkbox>
+                        </div>
+                    </div>
+                    <div ng-show="vm.gridSettings">
                         <div flex
                              ng-required="false"
                              md-color-picker
-                             ng-model="vm.gridSettings.titleColor"
-                             label="{{ 'dashboard.title-color' | translate }}"
+                             ng-model="vm.gridSettings.color"
+                             label="{{ 'layout.color' | translate }}"
                              icon="format_color_fill"
                              default="rgba(0, 0, 0, 0.870588)"
                              md-color-clear-button="false"
@@ -47,98 +88,87 @@
                              md-color-generic-palette="false"
                              md-color-history="false"
                         ></div>
-                    </div>
-                    <div layout="row" layout-align="start center">
-                        <md-checkbox flex aria-label="{{ 'dashboard.display-entities-selection' | translate }}"
-                                     ng-model="vm.gridSettings.showEntitiesSelect">{{ 'dashboard.display-entities-selection' | translate }}
-                        </md-checkbox>
-                        <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
-                                     ng-model="vm.gridSettings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
-                        </md-checkbox>
-                        <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
-                                     ng-model="vm.gridSettings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
-                        </md-checkbox>
-                    </div>
-                    <md-input-container class="md-block">
-                        <label translate>dashboard.columns-count</label>
-                        <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
-                               max="1000" />
-                        <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
-                            <div ng-message="required" translate>dashboard.columns-count-required</div>
-                            <div ng-message="min" translate>dashboard.min-columns-count-message</div>
-                            <div ng-message="max">dashboard.max-columns-count-message</div>
-                        </div>
-                    </md-input-container>
-                    <small translate>dashboard.widgets-margins</small>
-                    <div flex layout="row">
-                        <md-input-container flex class="md-block">
-                            <label translate>dashboard.horizontal-margin</label>
-                            <input required type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
-                                   max="50" />
-                            <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
-                                <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
-                                <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
-                                <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
-                            </div>
-                        </md-input-container>
-                        <md-input-container flex class="md-block">
-                            <label translate>dashboard.vertical-margin</label>
-                            <input required type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
-                                   max="50" />
-                            <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
-                                <div ng-message="required" translate>dashboard.vertical-margin-required</div>
-                                <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
-                                <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+                        <md-input-container class="md-block">
+                            <label translate>dashboard.columns-count</label>
+                            <input ng-required="vm.gridSettings" type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
+                                   max="1000" />
+                            <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
+                                <div ng-message="required" translate>dashboard.columns-count-required</div>
+                                <div ng-message="min" translate>dashboard.min-columns-count-message</div>
+                                <div ng-message="max">dashboard.max-columns-count-message</div>
                             </div>
                         </md-input-container>
-                    </div>
-                    <div flex
-                         ng-required="false"
-                         md-color-picker
-                         ng-model="vm.gridSettings.backgroundColor"
-                         label="{{ 'dashboard.background-color' | translate }}"
-                         icon="format_color_fill"
-                         default="rgba(0,0,0,0)"
-                         md-color-clear-button="false"
-                         open-on-input="true"
-                         md-color-generic-palette="false"
-                         md-color-history="false"
-                    ></div>
-                    <div class="tb-container">
-                        <label class="tb-label" translate>dashboard.background-image</label>
-                        <div flow-init="{singleFile:true}"
-                             flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
-                            <div class="tb-image-preview-container">
-                                <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
-                                <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
-                            </div>
-                            <div class="tb-image-clear-container">
-                                <md-button ng-click="vm.clearImage()"
-                                           class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
-                                    <md-tooltip md-direction="top">
-                                        {{ 'action.remove' | translate }}
-                                    </md-tooltip>
-                                    <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
-                                        close
-                                    </md-icon>
-                                </md-button>
-                            </div>
-                            <div class="alert tb-flow-drop" flow-drop>
-                                <label for="select" translate>dashboard.drop-image</label>
-                                <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+                        <small translate>dashboard.widgets-margins</small>
+                        <div flex layout="row">
+                            <md-input-container flex class="md-block">
+                                <label translate>dashboard.horizontal-margin</label>
+                                <input ng-required="vm.gridSettings" type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
+                                       max="50" />
+                                <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
+                                    <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
+                                    <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
+                                    <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
+                                </div>
+                            </md-input-container>
+                            <md-input-container flex class="md-block">
+                                <label translate>dashboard.vertical-margin</label>
+                                <input ng-required="vm.gridSettings" type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
+                                       max="50" />
+                                <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
+                                    <div ng-message="required" translate>dashboard.vertical-margin-required</div>
+                                    <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
+                                    <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+                                </div>
+                            </md-input-container>
+                        </div>
+                        <div flex
+                             ng-required="false"
+                             md-color-picker
+                             ng-model="vm.gridSettings.backgroundColor"
+                             label="{{ 'dashboard.background-color' | translate }}"
+                             icon="format_color_fill"
+                             default="rgba(0,0,0,0)"
+                             md-color-clear-button="false"
+                             open-on-input="true"
+                             md-color-generic-palette="false"
+                             md-color-history="false"
+                        ></div>
+                        <div class="tb-container">
+                            <label class="tb-label" translate>dashboard.background-image</label>
+                            <div flow-init="{singleFile:true}"
+                                 flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
+                                <div class="tb-image-preview-container">
+                                    <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
+                                    <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
+                                </div>
+                                <div class="tb-image-clear-container">
+                                    <md-button ng-click="vm.clearImage()"
+                                               class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+                                        <md-tooltip md-direction="top">
+                                            {{ 'action.remove' | translate }}
+                                        </md-tooltip>
+                                        <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+                                            close
+                                        </md-icon>
+                                    </md-button>
+                                </div>
+                                <div class="alert tb-flow-drop" flow-drop>
+                                    <label for="select" translate>dashboard.drop-image</label>
+                                    <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+                                </div>
                             </div>
                         </div>
+                        <md-input-container class="md-block">
+                            <label translate>dashboard.background-size-mode</label>
+                            <md-select ng-model="vm.gridSettings.backgroundSizeMode" placeholder="{{ 'dashboard.background-size-mode' | translate }}">
+                                <md-option value="100%">Fit width</md-option>
+                                <md-option value="auto 100%">Fit height</md-option>
+                                <md-option value="cover">Cover</md-option>
+                                <md-option value="contain">Contain</md-option>
+                                <md-option value="auto">Original size</md-option>
+                            </md-select>
+                        </md-input-container>
                     </div>
-                    <md-input-container class="md-block">
-                        <label translate>dashboard.background-size-mode</label>
-                        <md-select ng-model="vm.gridSettings.backgroundSizeMode" placeholder="{{ 'dashboard.background-size-mode' | translate }}">
-                            <md-option value="100%">Fit width</md-option>
-                            <md-option value="auto 100%">Fit height</md-option>
-                            <md-option value="cover">Cover</md-option>
-                            <md-option value="contain">Contain</md-option>
-                            <md-option value="auto">Original size</md-option>
-                        </md-select>
-                    </md-input-container>
                 </fieldset>
             </div>
         </md-dialog-content>
diff --git a/ui/src/app/dashboard/dashboard-toolbar.directive.js b/ui/src/app/dashboard/dashboard-toolbar.directive.js
new file mode 100644
index 0000000..63a34f8
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-toolbar.directive.js
@@ -0,0 +1,88 @@
+/*
+ * 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 './dashboard-toolbar.scss';
+
+import 'javascript-detect-element-resize/detect-element-resize';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardToolbarTemplate from './dashboard-toolbar.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardToolbar() {
+    return {
+        restrict: "E",
+        scope: true,
+        transclude: true,
+        bindToController: {
+            toolbarOpened: '=',
+            forceFullscreen: '=',
+            onTriggerClick: '&'
+        },
+        controller: DashboardToolbarController,
+        controllerAs: 'vm',
+        templateUrl: dashboardToolbarTemplate
+    };
+}
+
+/* eslint-disable angular/angularelement */
+
+
+/*@ngInject*/
+function DashboardToolbarController($scope, $element, $timeout, mdFabToolbarAnimation) {
+
+    let vm = this;
+
+    vm.mdFabToolbarElement = angular.element($element[0].querySelector('md-fab-toolbar'));
+
+    $timeout(function() {
+        vm.mdFabBackgroundElement = angular.element(vm.mdFabToolbarElement[0].querySelector('.md-fab-toolbar-background'));
+        vm.mdFabTriggerElement = angular.element(vm.mdFabToolbarElement[0].querySelector('md-fab-trigger button'));
+    });
+
+    addResizeListener(vm.mdFabToolbarElement[0], triggerFabResize); // eslint-disable-line no-undef
+
+    $scope.$on("$destroy", function () {
+        removeResizeListener(vm.mdFabToolbarElement[0], triggerFabResize); // eslint-disable-line no-undef
+    });
+
+    function triggerFabResize() {
+        var ctrl = vm.mdFabToolbarElement.controller('mdFabToolbar');
+        if (ctrl.isOpen) {
+            if (!vm.mdFabBackgroundElement[0].offsetWidth) {
+                mdFabToolbarAnimation.addClass(vm.mdFabToolbarElement, 'md-is-open', function () {
+                });
+            } else {
+                var color = window.getComputedStyle(vm.mdFabTriggerElement[0]).getPropertyValue('background-color'); //eslint-disable-line
+
+                var width = vm.mdFabToolbarElement[0].offsetWidth;
+                var scale = 2 * (width / vm.mdFabTriggerElement[0].offsetWidth);
+                vm.mdFabBackgroundElement[0].style.backgroundColor = color;
+                vm.mdFabBackgroundElement[0].style.borderRadius = width + 'px';
+
+                var transform = vm.mdFabBackgroundElement[0].style.transform;
+                var targetTransform = 'scale(' + scale + ')';
+                if (!transform || !angular.equals(transform, targetTransform)) {
+                    vm.mdFabBackgroundElement[0].style.transform = targetTransform;
+                }
+            }
+        }
+    }
+}
+
diff --git a/ui/src/app/dashboard/dashboard-toolbar.scss b/ui/src/app/dashboard/dashboard-toolbar.scss
new file mode 100644
index 0000000..af4889e
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-toolbar.scss
@@ -0,0 +1,114 @@
+/**
+ * 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 "~compass-sass-mixins/lib/compass";
+@import '../../scss/constants';
+
+tb-dashboard-toolbar {
+  md-fab-toolbar {
+    &.md-is-open {
+      md-fab-trigger {
+        .md-button {
+          &.md-fab {
+            opacity: 1;
+            @include transition(opacity .3s cubic-bezier(.55,0,.55,.2));
+            .md-fab-toolbar-background {
+                background-color: $primary-default !important;
+            }
+          }
+        }
+      }
+    }
+    md-fab-trigger {
+      .md-button {
+        &.md-fab {
+          line-height: 36px;
+          width: 36px;
+          height: 36px;
+          margin: 4px 0 0 4px;
+          opacity: 0.5;
+          @include transition(opacity .3s cubic-bezier(.55,0,.55,.2) .2s);
+          md-icon {
+            position: absolute;
+            top: 25%;
+            margin: 0;
+            line-height: 18px;
+            height: 18px;
+            width: 18px;
+            min-height: 18px;
+            min-width: 18px;
+          }
+        }
+      }
+    }
+    &.is-fullscreen {
+      &.md-is-open {
+        md-fab-trigger {
+          .md-button {
+            &.md-fab {
+              .md-fab-toolbar-background {
+                  transition-delay: 0ms !important;
+                  transition-duration: 0ms !important;
+              }
+            }
+          }
+        }
+      }
+      .md-fab-toolbar-wrapper {
+        height: 64px;
+        md-toolbar {
+          min-height: 64px;
+          height: 64px;
+        }
+      }
+    }
+    .md-fab-toolbar-wrapper {
+      height: 50px;
+      md-toolbar {
+        min-height: 50px;
+        height: 50px;
+        md-fab-actions {
+          font-size: 16px;
+          margin-top: 0px;
+          .close-action {
+            margin-right: -18px;
+          }
+          .md-fab-action-item {
+            width: 100%;
+            height: 46px;
+            .tb-dashboard-action-panels {
+              height: 46px;
+              flex-direction: row-reverse;
+              .tb-dashboard-action-panel {
+                height: 46px;
+                flex-direction: row-reverse;
+                div {
+                  height: 46px;
+                }
+                md-select {
+                  pointer-events: all;
+                }
+                tb-states-component {
+                  pointer-events: all;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/dashboard-toolbar.tpl.html b/ui/src/app/dashboard/dashboard-toolbar.tpl.html
new file mode 100644
index 0000000..46192ff
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-toolbar.tpl.html
@@ -0,0 +1,35 @@
+<!--
+
+    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-fab-toolbar md-open="vm.toolbarOpened"
+                md-direction="left"
+                ng-class="{'is-fullscreen': vm.forceFullscreen, 'md-whiteframe-z1': vm.forceFullscreen}">
+    <md-fab-trigger class="align-with-text">
+        <md-button aria-label="menu" class="md-fab md-primary" ng-click="vm.onTriggerClick()">
+            <md-tooltip ng-show="!vm.toolbarOpened" md-direction="bottom">
+                {{ 'dashboard.open-toolbar' | translate }}
+            </md-tooltip>
+            <md-icon aria-label="dashboard-toolbar" class="material-icons">more_horiz</md-icon>
+        </md-button>
+    </md-fab-trigger>
+    <md-toolbar>
+        <md-fab-actions class="md-toolbar-tools">
+            <div ng-transclude></div>
+        </md-fab-actions>
+    </md-toolbar>
+</md-fab-toolbar>
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 1256065..7cba4ee 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -34,7 +34,10 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
                     scope.widget.isSystemType).then(
                     function(widgetInfo) {
                         scope.$applyAsync(function(scope) {
-                            scope.widgetConfig = scope.widget.config;
+                            scope.widgetConfig = {
+                                config: scope.widget.config,
+                                layout: scope.widgetLayout
+                            };
                             var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
                             var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
                             scope.isDataEnabled = !widgetInfo.useCustomDatasources;
@@ -58,6 +61,12 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
             }
         });
 
+        scope.$watch('widgetLayout', function () {
+            if (scope.widgetLayout && scope.widgetConfig) {
+                scope.widgetConfig.layout = scope.widgetLayout;
+            }
+        });
+
         scope.fetchEntityKeys = function (entityAliasId, query, type) {
             var entityAlias = scope.aliasesInfo.entityAliases[entityAliasId];
             if (entityAlias && entityAlias.entityId) {
@@ -117,6 +126,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
             dashboard: '=',
             aliasesInfo: '=',
             widget: '=',
+            widgetLayout: '=',
             theForm: '='
         }
     };
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index c8ba734..d940f09 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -16,7 +16,6 @@
 import './dashboard.scss';
 
 import uiRouter from 'angular-ui-router';
-import gridster from 'angular-gridster';
 
 import thingsboardGrid from '../components/grid.directive';
 import thingsboardApiWidget from '../api/widget.service';
@@ -26,6 +25,7 @@ import thingsboardApiCustomer from '../api/customer.service';
 import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
 import thingsboardWidgetConfig from '../components/widget-config.directive';
 import thingsboardDashboardSelect from '../components/dashboard-select.directive';
+import thingsboardRelatedEntityAutocomplete from '../components/related-entity-autocomplete.directive';
 import thingsboardDashboard from '../components/dashboard.directive';
 import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
 import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
@@ -33,6 +33,8 @@ import thingsboardSocialsharePanel from '../components/socialshare-panel.directi
 import thingsboardTypes from '../common/types.constant';
 import thingsboardItemBuffer from '../services/item-buffer.service';
 import thingsboardImportExport from '../import-export';
+import dashboardLayouts from './layouts';
+import dashboardStates from './states';
 
 import DashboardRoutes from './dashboard.routes';
 import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
@@ -43,10 +45,10 @@ import AddDashboardsToCustomerController from './add-dashboards-to-customer.cont
 import AddWidgetController from './add-widget.controller';
 import DashboardDirective from './dashboard.directive';
 import EditWidgetDirective from './edit-widget.directive';
+import DashboardToolbar from './dashboard-toolbar.directive';
 
 export default angular.module('thingsboard.dashboard', [
     uiRouter,
-    gridster.name,
     thingsboardTypes,
     thingsboardItemBuffer,
     thingsboardImportExport,
@@ -58,10 +60,13 @@ export default angular.module('thingsboard.dashboard', [
     thingsboardDetailsSidenav,
     thingsboardWidgetConfig,
     thingsboardDashboardSelect,
+    thingsboardRelatedEntityAutocomplete,
     thingsboardDashboard,
     thingsboardExpandFullscreen,
     thingsboardWidgetsBundleSelect,
-    thingsboardSocialsharePanel
+    thingsboardSocialsharePanel,
+    dashboardLayouts,
+    dashboardStates
 ])
     .config(DashboardRoutes)
     .controller('DashboardsController', DashboardsController)
@@ -74,4 +79,5 @@ export default angular.module('thingsboard.dashboard', [
     .controller('AddWidgetController', AddWidgetController)
     .directive('tbDashboardDetails', DashboardDirective)
     .directive('tbEditWidget', EditWidgetDirective)
+    .directive('tbDashboardToolbar', DashboardToolbar)
     .name;
diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.directive.js b/ui/src/app/dashboard/layouts/dashboard-layout.directive.js
new file mode 100644
index 0000000..18926c3
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/dashboard-layout.directive.js
@@ -0,0 +1,277 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardLayoutTemplate from './dashboard-layout.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardLayout() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            layoutCtx: '=',
+            dashboardCtx: '=',
+            isEdit: '=',
+            isMobile: '=',
+            widgetEditMode: '=',
+            getStDiff: '&?'
+        },
+        controller: DashboardLayoutController,
+        controllerAs: 'vm',
+        templateUrl: dashboardLayoutTemplate
+    };
+}
+
+/*@ngInject*/
+function DashboardLayoutController($scope, $rootScope, $translate, $window, hotkeys, itembuffer) {
+
+    var vm = this;
+
+    vm.noData = noData;
+    vm.addWidget = addWidget;
+    vm.editWidget = editWidget;
+    vm.exportWidget = exportWidget;
+    vm.widgetMouseDown = widgetMouseDown;
+    vm.widgetClicked = widgetClicked;
+    vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
+    vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
+    vm.removeWidget = removeWidget;
+    vm.pasteWidget = pasteWidget;
+    vm.pasteWidgetReference = pasteWidgetReference;
+    vm.dashboardInited = dashboardInited;
+    vm.dashboardInitFailed = dashboardInitFailed;
+
+    vm.reload = function() {
+        if (vm.dashboardContainer) {
+            vm.dashboardContainer.reload();
+        }
+    };
+
+    vm.setResizing = function(resizing) {
+        if (vm.dashboardContainer) {
+            vm.dashboardContainer.isResizing = resizing;
+        }
+    }
+
+    vm.resetHighlight = function() {
+        if (vm.dashboardContainer) {
+            vm.dashboardContainer.resetHighlight();
+        }
+    };
+
+    vm.highlightWidget = function(widget, delay) {
+        if (vm.dashboardContainer) {
+            vm.dashboardContainer.highlightWidget(widget, delay);
+        }
+    };
+
+    vm.selectWidget = function(widget, delay) {
+        if (vm.dashboardContainer) {
+            vm.dashboardContainer.selectWidget(widget, delay);
+        }
+    };
+
+    vm.dashboardInitComplete = false;
+
+    initHotKeys();
+
+    $scope.$on('$destroy', function() {
+        vm.dashboardContainer = null;
+    });
+
+    $scope.$watch('vm.layoutCtx', function () {
+        if (vm.layoutCtx) {
+            vm.layoutCtx.ctrl = vm;
+        }
+    });
+
+    function noData() {
+        return vm.dashboardInitComplete && vm.layoutCtx &&
+            vm.layoutCtx.widgets && vm.layoutCtx.widgets.length == 0;
+    }
+
+    function addWidget($event) {
+        if (vm.dashboardCtx.onAddWidget) {
+            vm.dashboardCtx.onAddWidget($event, vm.layoutCtx);
+        }
+    }
+
+    function editWidget($event, widget) {
+        if (vm.dashboardCtx.onEditWidget) {
+            vm.dashboardCtx.onEditWidget($event, vm.layoutCtx, widget);
+        }
+    }
+
+    function exportWidget($event, widget) {
+        if (vm.dashboardCtx.onExportWidget) {
+            vm.dashboardCtx.onExportWidget($event, vm.layoutCtx, widget);
+        }
+    }
+
+    function widgetMouseDown($event, widget) {
+        if (vm.dashboardCtx.onWidgetMouseDown) {
+            vm.dashboardCtx.onWidgetMouseDown($event, vm.layoutCtx, widget);
+        }
+    }
+
+    function widgetClicked($event, widget) {
+        if (vm.dashboardCtx.onWidgetClicked) {
+            vm.dashboardCtx.onWidgetClicked($event, vm.layoutCtx, widget);
+        }
+    }
+
+    function prepareDashboardContextMenu() {
+        if (vm.dashboardCtx.prepareDashboardContextMenu) {
+            return vm.dashboardCtx.prepareDashboardContextMenu(vm.layoutCtx);
+        }
+    }
+
+    function prepareWidgetContextMenu(widget) {
+        if (vm.dashboardCtx.prepareWidgetContextMenu) {
+            return vm.dashboardCtx.prepareWidgetContextMenu(vm.layoutCtx, widget);
+        }
+    }
+
+    function removeWidget($event, widget) {
+        if (vm.dashboardCtx.onRemoveWidget) {
+            vm.dashboardCtx.onRemoveWidget($event, vm.layoutCtx, widget);
+        }
+    }
+
+    function dashboardInitFailed() {
+        var parentScope = $window.parent.angular.element($window.frameElement).scope();
+        parentScope.$emit('widgetEditModeInited');
+        parentScope.$apply();
+        vm.dashboardInitComplete = true;
+    }
+
+    function dashboardInited(dashboardContainer) {
+        vm.dashboardContainer = dashboardContainer;
+        vm.dashboardInitComplete = true;
+    }
+
+    function isHotKeyAllowed(event) {
+        var target = event.target || event.srcElement;
+        var scope = angular.element(target).scope();
+        return scope && scope.$parent !== $rootScope;
+    }
+
+    function initHotKeys() {
+        $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
+            hotkeys.bindTo($scope)
+                .add({
+                    combo: 'ctrl+c',
+                    description: translations['action.copy'],
+                    callback: function (event) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            var widget = vm.dashboardContainer.getSelectedWidget();
+                            if (widget) {
+                                event.preventDefault();
+                                copyWidget(event, widget);
+                            }
+                        }
+                    }
+                })
+                .add({
+                    combo: 'ctrl+r',
+                    description: translations['action.copy-reference'],
+                    callback: function (event) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            var widget = vm.dashboardContainer.getSelectedWidget();
+                            if (widget) {
+                                event.preventDefault();
+                                copyWidgetReference(event, widget);
+                            }
+                        }
+                    }
+                })
+                .add({
+                    combo: 'ctrl+v',
+                    description: translations['action.paste'],
+                    callback: function (event) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            if (itembuffer.hasWidget()) {
+                                event.preventDefault();
+                                pasteWidget(event);
+                            }
+                        }
+                    }
+                })
+                .add({
+                    combo: 'ctrl+i',
+                    description: translations['action.paste-reference'],
+                    callback: function (event) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            if (itembuffer.canPasteWidgetReference(vm.dashboardCtx.dashboard,
+                                    vm.dashboardCtx.state, vm.layoutCtx.id)) {
+                                event.preventDefault();
+                                pasteWidgetReference(event);
+                            }
+                        }
+                    }
+                })
+
+                .add({
+                    combo: 'ctrl+x',
+                    description: translations['action.delete'],
+                    callback: function (event) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            var widget = vm.dashboardContainer.getSelectedWidget();
+                            if (widget) {
+                                event.preventDefault();
+                                vm.dashboardCtx.onRemoveWidget(event, vm.layoutCtx, widget);
+                            }
+                        }
+                    }
+                });
+        });
+    }
+
+    function copyWidget($event, widget) {
+        if (vm.dashboardCtx.copyWidget) {
+            vm.dashboardCtx.copyWidget($event, vm.layoutCtx, widget);
+        }
+    }
+
+    function copyWidgetReference($event, widget) {
+        if (vm.dashboardCtx.copyWidgetReference) {
+            vm.dashboardCtx.copyWidgetReference($event, vm.layoutCtx, widget);
+        }
+    }
+
+    function pasteWidget($event) {
+        var pos = vm.dashboardContainer.getEventGridPosition($event);
+        if (vm.dashboardCtx.pasteWidget) {
+            vm.dashboardCtx.pasteWidget($event, vm.layoutCtx, pos);
+        }
+    }
+
+    function pasteWidgetReference($event) {
+        var pos = vm.dashboardContainer.getEventGridPosition($event);
+        if (vm.dashboardCtx.pasteWidgetReference) {
+            vm.dashboardCtx.pasteWidgetReference($event, vm.layoutCtx, pos);
+        }
+    }
+
+}
diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
new file mode 100644
index 0000000..ea84858
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
@@ -0,0 +1,69 @@
+<!--
+
+    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 style="position: relative; width: 100%; height: 100%;"
+            ng-style="{'background-color': vm.layoutCtx.gridSettings.backgroundColor,
+                    'background-image': 'url('+vm.layoutCtx.gridSettings.backgroundImageUrl+')',
+                    'background-repeat': 'no-repeat',
+                    'background-attachment': 'scroll',
+                    'background-size': vm.layoutCtx.gridSettings.backgroundSizeMode || '100%',
+                    'background-position': '0% 0%'}">
+    <section ng-show="!loading && vm.noData()" layout-align="center center"
+             ng-style="{'color': vm.layoutCtx.gridSettings.color}"
+             style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;"
+             class="md-headline tb-absolute-fill">
+            <span translate ng-if="!vm.isEdit">
+                dashboard.no-widgets
+            </span>
+        <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget({event: $event})">
+            <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
+            {{ 'dashboard.add-widget' | translate }}
+        </md-button>
+    </section>
+    <tb-dashboard
+            dashboard-style="{'background-color': vm.layoutCtx.gridSettings.backgroundColor,
+                            'background-image': 'url('+vm.layoutCtx.gridSettings.backgroundImageUrl+')',
+                            'background-repeat': 'no-repeat',
+                            'background-attachment': 'scroll',
+                            'background-size': vm.layoutCtx.gridSettings.backgroundSizeMode || '100%',
+                            'background-position': '0% 0%'}"
+            widgets="vm.layoutCtx.widgets"
+            widget-layouts="vm.layoutCtx.widgetLayouts"
+            columns="vm.layoutCtx.gridSettings.columns"
+            margins="vm.layoutCtx.gridSettings.margins"
+            aliases-info="vm.dashboardCtx.aliasesInfo"
+            state-controller="vm.dashboardCtx.stateController"
+            dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow"
+            is-edit="vm.isEdit"
+            is-mobile="vm.isMobile"
+            is-mobile-disabled="vm.widgetEditMode"
+            is-edit-action-enabled="vm.isEdit"
+            is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
+            is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
+            on-edit-widget="vm.editWidget(event, widget)"
+            on-export-widget="vm.exportWidget(event, widget)"
+            on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
+            on-widget-clicked="vm.widgetClicked(event, widget)"
+            prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
+            prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
+            on-remove-widget="vm.removeWidget(event, widget)"
+            get-st-diff="vm.getStDiff()"
+            on-init="vm.dashboardInited(dashboard)"
+            on-init-failed="vm.dashboardInitFailed(e)"
+            ignore-loading="vm.layoutCtx.ignoreLoading">
+    </tb-dashboard>
+</md-content>
diff --git a/ui/src/app/dashboard/layouts/index.js b/ui/src/app/dashboard/layouts/index.js
new file mode 100644
index 0000000..6315565
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/index.js
@@ -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.
+ */
+
+import ManageDashboardLayoutsController from './manage-dashboard-layouts.controller';
+import SelectTargetLayoutController from './select-target-layout.controller';
+import DashboardLayoutDirective from './dashboard-layout.directive';
+
+export default angular.module('thingsboard.dashboard.layouts', [])
+    .controller('ManageDashboardLayoutsController', ManageDashboardLayoutsController)
+    .controller('SelectTargetLayoutController', SelectTargetLayoutController)
+    .directive('tbDashboardLayout', DashboardLayoutDirective)
+    .name;
diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
new file mode 100644
index 0000000..1ccf48f
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardSettingsTemplate from '../dashboard-settings.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ManageDashboardLayoutsController($scope, $mdDialog, $document, dashboardUtils, layouts) {
+
+    var vm = this;
+
+    vm.openLayoutSettings = openLayoutSettings;
+    vm.cancel = cancel;
+    vm.save = save;
+
+    vm.layouts = layouts;
+    vm.displayLayouts = {
+        main: angular.isDefined(vm.layouts.main),
+        right: angular.isDefined(vm.layouts.right)
+    }
+
+    for (var l in vm.displayLayouts) {
+        if (!vm.layouts[l]) {
+            vm.layouts[l] = dashboardUtils.createDefaultLayoutData();
+        }
+    }
+
+    function openLayoutSettings($event, layoutId) {
+        var gridSettings = angular.copy(vm.layouts[layoutId].gridSettings);
+        $mdDialog.show({
+            controller: 'DashboardSettingsController',
+            controllerAs: 'vm',
+            templateUrl: dashboardSettingsTemplate,
+            locals: {
+                settings: null,
+                gridSettings: gridSettings
+            },
+            parent: angular.element($document[0].body),
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (data) {
+            var gridSettings = data.gridSettings;
+            if (gridSettings) {
+                dashboardUtils.updateLayoutSettings(vm.layouts[layoutId], gridSettings);
+            }
+            $scope.theForm.$setDirty();
+        }, function () {
+        });
+    }
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function save() {
+        $scope.theForm.$setPristine();
+        for (var l in vm.displayLayouts) {
+            if (!vm.displayLayouts[l]) {
+                if (vm.layouts[l]) {
+                    delete vm.layouts[l];
+                }
+            }
+        }
+        $mdDialog.hide(vm.layouts);
+    }
+}
diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html
new file mode 100644
index 0000000..d4d95db
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html
@@ -0,0 +1,65 @@
+<!--
+
+    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="{{ 'layout.manage' | translate }}">
+    <form name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>{{ 'layout.manage' }}</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">
+                <fieldset ng-disabled="loading">
+                    <div layout="row" layout-align="start center">
+                        <md-checkbox ng-disabled="true" flex aria-label="{{ 'layout.main' | translate }}"
+                                     ng-model="vm.displayLayouts.main">{{ 'layout.main' | translate }}
+                        </md-checkbox>
+                        <md-checkbox flex aria-label="{{ 'layout.right' | translate }}"
+                                     ng-model="vm.displayLayouts.right">{{ 'layout.right' | translate }}
+                        </md-checkbox>
+                    </div>
+                    <div layout="row" layout-align="start center">
+                        <md-button flex ng-show="vm.displayLayouts.main"
+                                   class="tb-layout-button md-raised md-primary" layout="column"
+                                   ng-click="vm.openLayoutSettings($event, 'main')">
+                            <span translate>layout.main</span>
+                        </md-button>
+                        <md-button flex ng-show="vm.displayLayouts.right"
+                                   class="tb-layout-button md-raised md-primary" layout="column"
+                                   ng-click="vm.openLayoutSettings($event, 'right')">
+                            <span translate>layout.right</span>
+                        </md-button>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+                {{ 'action.save' | 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/dashboard/layouts/select-target-layout.tpl.html b/ui/src/app/dashboard/layouts/select-target-layout.tpl.html
new file mode 100644
index 0000000..f69f487
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/select-target-layout.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+    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="{{ 'layout.select' | translate }}">
+    <form name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>{{ 'layout.select' }}</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">
+                <fieldset ng-disabled="loading">
+                    <div layout="row" layout-align="start center">
+                        <md-button flex class="tb-layout-button md-raised md-primary" layout="column"
+                                   ng-click="vm.selectLayout($event, 'main')">
+                            <span translate>layout.main</span>
+                        </md-button>
+                        <md-button flex class="tb-layout-button md-raised md-primary" layout="column"
+                                   ng-click="vm.selectLayout($event, 'right')">
+                            <span translate>layout.right</span>
+                        </md-button>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
new file mode 100644
index 0000000..86eb9ec
--- /dev/null
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
@@ -0,0 +1,82 @@
+/*
+ * 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 DashboardStateDialogController($scope, $mdDialog, $filter, dashboardUtils, isAdd, allStates, state) {
+
+    var vm = this;
+
+    vm.isAdd = isAdd;
+    vm.allStates = allStates;
+    vm.state = state;
+
+    vm.stateIdTouched = false;
+
+    if (vm.isAdd) {
+        vm.state = dashboardUtils.createDefaultState('', false);
+        vm.state.id = '';
+        vm.prevStateId = '';
+    } else {
+        vm.state = state;
+        vm.prevStateId = vm.state.id;
+    }
+
+    vm.cancel = cancel;
+    vm.save = save;
+
+    $scope.$watch("vm.state.name", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.state.name != null) {
+            checkStateName();
+        }
+    });
+
+    $scope.$watch("vm.state.id", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.state.id != null) {
+            checkStateId();
+        }
+    });
+
+    function checkStateName() {
+        if (!vm.stateIdTouched && vm.isAdd) {
+            vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_");
+        }
+        var result = $filter('filter')(vm.allStates, {name: vm.state.name}, true);
+        if (result && result.length && result[0].id !== vm.prevStateId) {
+            $scope.theForm.name.$setValidity('stateExists', false);
+        } else {
+            $scope.theForm.name.$setValidity('stateExists', true);
+        }
+    }
+
+    function checkStateId() {
+        var result = $filter('filter')(vm.allStates, {id: vm.state.id}, true);
+        if (result && result.length && result[0].id !== vm.prevStateId) {
+            $scope.theForm.stateId.$setValidity('stateExists', false);
+        } else {
+            $scope.theForm.stateId.$setValidity('stateExists', true);
+        }
+    }
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function save() {
+        $scope.theForm.$setPristine();
+        vm.state.id = vm.state.id.trim();
+        $mdDialog.hide(vm.state);
+    }
+}
diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js
new file mode 100644
index 0000000..782f59e
--- /dev/null
+++ b/ui/src/app/dashboard/states/default-state-controller.js
@@ -0,0 +1,181 @@
+/*
+ * 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 DefaultStateController($scope, $location, $state, $stateParams, $translate, types, dashboardUtils) {
+
+    var vm = this;
+
+    vm.inited = false;
+
+    vm.openState = openState;
+    vm.updateState = updateState;
+    vm.navigatePrevState = navigatePrevState;
+    vm.getStateId = getStateId;
+    vm.getStateParams = getStateParams;
+
+    vm.getStateName = getStateName;
+
+    vm.displayStateSelection = displayStateSelection;
+
+    function openState(id, params) {
+        if (vm.states && vm.states[id]) {
+            if (!params) {
+                params = {};
+            }
+            var newState = {
+                id: id,
+                params: params
+            }
+            //append new state
+            vm.stateObject[0] = newState;
+            gotoState(vm.stateObject[0].id, true);
+        }
+    }
+
+    function updateState(id, params) {
+        if (vm.states && vm.states[id]) {
+            if (!params) {
+                params = {};
+            }
+            var newState = {
+                id: id,
+                params: params
+            }
+            //replace with new state
+            vm.stateObject[0] = newState;
+            gotoState(vm.stateObject[0].id, true);
+        }
+    }
+
+    function navigatePrevState(index) {
+        if (index < vm.stateObject.length-1) {
+            vm.stateObject.splice(index+1, vm.stateObject.length-index-1);
+            gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+        }
+    }
+
+    function getStateId() {
+        return vm.stateObject[vm.stateObject.length-1].id;
+    }
+
+    function getStateParams() {
+        return vm.stateObject[vm.stateObject.length-1].params;
+    }
+
+    function getStateName(id, state) {
+        var result = '';
+        var translationId = types.translate.dashboardStatePrefix + id;
+        var translation = $translate.instant(translationId);
+        if (translation != translationId) {
+            result = translation;
+        } else {
+            result = state.name;
+        }
+        return result;
+    }
+
+    function parseState(stateJson) {
+        var result;
+        if (stateJson) {
+            try {
+                result = angular.fromJson(stateJson);
+            } catch (e) {
+                result = [ { id: null, params: {} } ];
+            }
+        }
+        if (!result) {
+            result = [];
+        }
+        if (!result.length) {
+            result[0] = { id: null, params: {} }
+        }
+        if (!result[0].id) {
+            result[0].id = dashboardUtils.getRootStateId(vm.states);
+        }
+        return result;
+    }
+
+    $scope.$watch('vm.states', function() {
+        if (vm.states) {
+            if (!vm.inited) {
+                vm.inited = true;
+                init();
+            }
+        }
+    });
+
+    function displayStateSelection() {
+        return vm.states && Object.keys(vm.states).length > 1;
+    }
+
+    function init() {
+        var initialState = $stateParams.state;
+        vm.stateObject = parseState(initialState);
+
+        gotoState(vm.stateObject[0].id, false);
+
+        $scope.$watchCollection(function(){
+            return $state.params;
+        }, function(){
+            var currentState = $state.params.state;
+            vm.stateObject = parseState(currentState);
+        });
+
+        $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function() {
+            if (vm.stateObject[0].id !== vm.dashboardCtrl.dashboardCtx.state) {
+                stopWatchStateObject();
+                vm.stateObject[0].id = vm.dashboardCtrl.dashboardCtx.state;
+                updateLocation();
+                watchStateObject();
+            }
+        });
+        watchStateObject();
+    }
+
+    function stopWatchStateObject() {
+        if (vm.stateObjectWatcher) {
+            vm.stateObjectWatcher();
+            vm.stateObjectWatcher = null;
+        }
+    }
+
+    function watchStateObject() {
+        vm.stateObjectWatcher = $scope.$watch('vm.stateObject', function(newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal) && newVal) {
+                gotoState(vm.stateObject[0].id, true);
+            }
+        }, true);
+    }
+
+    function gotoState(stateId, update) {
+        if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
+            vm.dashboardCtrl.openDashboardState(stateId);
+            if (update) {
+                updateLocation();
+            }
+        }
+    }
+
+    function updateLocation() {
+        if (vm.stateObject[0].id) {
+            $location.search({state : angular.toJson(vm.stateObject)});
+        }
+    }
+
+
+
+}
diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js
new file mode 100644
index 0000000..8ee9285
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.js
@@ -0,0 +1,243 @@
+/*
+ * 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-state-controller.scss';
+
+/*@ngInject*/
+export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, types, dashboardUtils, entityService) {
+
+    var vm = this;
+
+    vm.inited = false;
+
+    vm.openState = openState;
+    vm.updateState = updateState;
+    vm.navigatePrevState = navigatePrevState;
+    vm.getStateId = getStateId;
+    vm.getStateParams = getStateParams;
+
+    vm.getStateName = getStateName;
+
+    vm.selectedStateIndex = -1;
+
+    function openState(id, params) {
+        if (vm.states && vm.states[id]) {
+            resolveEntity(params).then(
+                function success(entityName) {
+                    params.entityName = entityName;
+                    var newState = {
+                        id: id,
+                        params: params
+                    }
+                    //append new state
+                    vm.stateObject.push(newState);
+                    vm.selectedStateIndex = vm.stateObject.length-1;
+                    gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+                }
+            );
+        }
+    }
+
+    function updateState(id, params) {
+        if (vm.states && vm.states[id]) {
+            resolveEntity(params).then(
+                function success(entityName) {
+                    params.entityName = entityName;
+                    var newState = {
+                        id: id,
+                        params: params
+                    }
+                    //replace with new state
+                    vm.stateObject[vm.stateObject.length - 1] = newState;
+                    gotoState(vm.stateObject[vm.stateObject.length - 1].id, true);
+                }
+            );
+        }
+    }
+
+    function navigatePrevState(index) {
+        if (index < vm.stateObject.length-1) {
+            vm.stateObject.splice(index+1, vm.stateObject.length-index-1);
+            vm.selectedStateIndex = vm.stateObject.length-1;
+            gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+        }
+    }
+
+    function getStateId() {
+        return vm.stateObject[vm.stateObject.length-1].id;
+    }
+
+    function getStateParams() {
+        return vm.stateObject[vm.stateObject.length-1].params;
+    }
+
+    function getStateName(index) {
+        var result = '';
+        if (vm.stateObject[index]) {
+            var params = vm.stateObject[index].params;
+            if (params && params.entityName) {
+                result = params.entityName;
+            } else {
+                var id = vm.stateObject[index].id;
+                var translationId = types.translate.dashboardStatePrefix + id;
+                var translation = $translate.instant(translationId);
+                if (translation != translationId) {
+                    result = translation;
+                } else {
+                    result = vm.states[vm.stateObject[index].id].name;
+                }
+            }
+        }
+        return result;
+    }
+
+    function resolveEntity(params) {
+        var deferred = $q.defer();
+        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 = entity.name;
+                    deferred.resolve(entityName);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        } else {
+            deferred.reject();
+        }
+        return deferred.promise;
+    }
+
+    function parseState(stateJson) {
+        var result;
+        if (stateJson) {
+            try {
+                result = angular.fromJson(stateJson);
+            } catch (e) {
+                result = [ { id: null, params: {} } ];
+            }
+        }
+        if (!result) {
+            result = [];
+        }
+        if (!result.length) {
+            result[0] = { id: null, params: {} }
+        }
+        if (!result[0].id) {
+            result[0].id = dashboardUtils.getRootStateId(vm.states);
+        }
+        return result;
+    }
+
+    $scope.$watch('vm.states', function() {
+        if (vm.states) {
+            if (!vm.inited) {
+                vm.inited = true;
+                init();
+            }
+        }
+    });
+
+    function init() {
+        var initialState = $stateParams.state;
+        vm.stateObject = parseState(initialState);
+        vm.selectedStateIndex = vm.stateObject.length-1;
+        gotoState(vm.stateObject[vm.stateObject.length-1].id, false);
+
+        $scope.$watchCollection(function() {
+            return $state.params;
+        }, function(){
+            var currentState = $state.params.state;
+            vm.stateObject = parseState(currentState);
+        });
+
+        $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function() {
+            if (vm.stateObject[vm.stateObject.length-1].id !== vm.dashboardCtrl.dashboardCtx.state) {
+                stopWatchStateObject();
+                vm.stateObject[vm.stateObject.length-1].id = vm.dashboardCtrl.dashboardCtx.state;
+                updateLocation();
+                watchStateObject();
+            }
+        });
+
+        watchStateObject();
+
+        if (vm.dashboardCtrl.isMobile) {
+            watchSelectedStateIndex();
+        }
+
+        $scope.$watch('vm.dashboardCtrl.isMobile', function(newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                if (vm.dashboardCtrl.isMobile) {
+                    watchSelectedStateIndex();
+                } else {
+                    stopWatchSelectedStateIndex();
+                }
+            }
+        });
+
+    }
+
+    function stopWatchStateObject() {
+        if (vm.stateObjectWatcher) {
+            vm.stateObjectWatcher();
+            vm.stateObjectWatcher = null;
+        }
+    }
+
+    function watchStateObject() {
+        vm.stateObjectWatcher = $scope.$watch('vm.stateObject', function(newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal) && newVal) {
+                vm.selectedStateIndex = vm.stateObject.length-1;
+                gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+            }
+        }, true);
+    }
+
+    function stopWatchSelectedStateIndex() {
+        if (vm.selectedStateIndexWatcher) {
+            vm.selectedStateIndexWatcher();
+            vm.selectedStateIndexWatcher = null;
+        }
+    }
+
+    function watchSelectedStateIndex() {
+        vm.selectedStateIndexWatcher = $scope.$watch('vm.selectedStateIndex', function(newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                navigatePrevState(vm.selectedStateIndex);
+            }
+        });
+    }
+
+    function gotoState(stateId, update) {
+        if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
+            vm.dashboardCtrl.openDashboardState(stateId);
+            if (update) {
+                updateLocation();
+            }
+        }
+    }
+
+    function updateLocation() {
+        if (vm.stateObject[vm.stateObject.length-1].id) {
+            $location.search({state : angular.toJson(vm.stateObject)});
+        }
+    }
+
+
+
+}
diff --git a/ui/src/app/dashboard/states/entity-state-controller.scss b/ui/src/app/dashboard/states/entity-state-controller.scss
new file mode 100644
index 0000000..2d79157
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.scss
@@ -0,0 +1,33 @@
+/**
+ * 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.
+ */
+
+.entity-state-controller {
+    .state-divider {
+        font-size: 28px;
+        padding-left: 15px;
+        padding-right: 15px;
+    }
+    .state-entry {
+        font-size: 22px;
+        outline: none;
+    }
+    md-select {
+        .md-text {
+            font-size: 22px;
+            font-weight: bold;
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/states/entity-state-controller.tpl.html b/ui/src/app/dashboard/states/entity-state-controller.tpl.html
new file mode 100644
index 0000000..4139be6
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+    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 class="entity-state-controller">
+    <div ng-if="!vm.dashboardCtrl.isMobile || vm.stateObject.length===1" layout="row" layout-align="start center">
+        <div layout="row" layout-align="start center" ng-repeat="state in vm.stateObject track by $index">
+            <span class='state-divider' ng-if="$index"> > </span>
+            <span class='state-entry' ng-style="{fontWeight: $last ? 'bold' : 'normal',
+                             cursor: $last ? 'default' : 'pointer'}" ng-click="vm.navigatePrevState($index)">
+                {{vm.getStateName($index)}}
+            </span>
+        </div>
+    </div>
+    <md-select ng-if="vm.dashboardCtrl.isMobile && vm.stateObject.length > 1" aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.selectedStateIndex">
+        <md-option ng-repeat="state in vm.stateObject track by $index" ng-value="$index">
+            {{vm.getStateName($index)}}
+        </md-option>
+    </md-select>
+</div>
\ No newline at end of file
diff --git a/ui/src/app/dashboard/states/index.js b/ui/src/app/dashboard/states/index.js
new file mode 100644
index 0000000..ce59e38
--- /dev/null
+++ b/ui/src/app/dashboard/states/index.js
@@ -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.
+ */
+
+import ManageDashboardStatesController from './manage-dashboard-states.controller';
+import DashboardStateDialogController from './dashboard-state-dialog.controller';
+import SelectTargetStateController from './select-target-state.controller';
+import StatesComponentDirective from './states-component.directive';
+import StatesControllerService from './states-controller.service';
+
+export default angular.module('thingsboard.dashboard.states', [])
+    .controller('ManageDashboardStatesController', ManageDashboardStatesController)
+    .controller('DashboardStateDialogController', DashboardStateDialogController)
+    .controller('SelectTargetStateController', SelectTargetStateController)
+    .directive('tbStatesComponent', StatesComponentDirective)
+    .factory('statesControllerService', StatesControllerService)
+    .name;
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
new file mode 100644
index 0000000..452e94f
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
@@ -0,0 +1,198 @@
+/*
+ * 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 './manage-dashboard-states.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardStateDialogTemplate from './dashboard-state-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ManageDashboardStatesController($scope, $mdDialog, $filter, $document, $translate, states) {
+
+    var vm = this;
+
+    vm.allStates = [];
+    for (var id in states) {
+        var state = states[id];
+        state.id = id;
+        vm.allStates.push(state);
+    }
+
+    vm.states = [];
+    vm.statesCount = 0;
+
+    vm.query = {
+        order: 'name',
+        limit: 5,
+        page: 1,
+        search: null
+    };
+
+    vm.enterFilterMode = enterFilterMode;
+    vm.exitFilterMode = exitFilterMode;
+    vm.onReorder = onReorder;
+    vm.onPaginate = onPaginate;
+    vm.addState = addState;
+    vm.editState = editState;
+    vm.deleteState = deleteState;
+
+    vm.cancel = cancel;
+    vm.save = save;
+
+    $scope.$watch("vm.query.search", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+            updateStates();
+        }
+    });
+
+    updateStates ();
+
+    function updateStates () {
+        var result = $filter('orderBy')(vm.allStates, vm.query.order);
+        if (vm.query.search != null) {
+            result = $filter('filter')(result, {$: vm.query.search});
+        }
+        vm.statesCount = result.length;
+        var startIndex = vm.query.limit * (vm.query.page - 1);
+        vm.states = result.slice(startIndex, startIndex + vm.query.limit);
+    }
+
+    function enterFilterMode () {
+        vm.query.search = '';
+    }
+
+    function exitFilterMode () {
+        vm.query.search = null;
+        updateStates();
+    }
+
+    function onReorder () {
+        updateStates();
+    }
+
+    function onPaginate () {
+        updateStates();
+    }
+
+    function addState ($event) {
+        openStateDialog($event, null, true);
+    }
+
+    function editState ($event, alertRule) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        openStateDialog($event, alertRule, false);
+    }
+
+    function openStateDialog($event, state, isAdd) {
+        var prevStateId = null;
+        if (!isAdd) {
+            prevStateId = state.id;
+        }
+        $mdDialog.show({
+            controller: 'DashboardStateDialogController',
+            controllerAs: 'vm',
+            templateUrl: dashboardStateDialogTemplate,
+            parent: angular.element($document[0].body),
+            locals: {isAdd: isAdd, allStates: vm.allStates, state: angular.copy(state)},
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (state) {
+            saveState(state, prevStateId);
+            updateStates();
+        });
+    }
+
+    function getStateIndex(id) {
+        var result = $filter('filter')(vm.allStates, {id: id});
+        if (result && result.length) {
+            return vm.allStates.indexOf(result[0]);
+        }
+        return -1;
+    }
+
+    function saveState(state, prevStateId) {
+        if (prevStateId) {
+            var index = getStateIndex(prevStateId);
+            if (index > -1) {
+                vm.allStates[index] = state;
+            }
+        } else {
+            vm.allStates.push(state);
+        }
+        if (state.root) {
+            for (var i=0; i < vm.allStates.length; i++) {
+                var otherState = vm.allStates[i];
+                if (otherState.id !== state.id) {
+                    otherState.root = false;
+                }
+            }
+        }
+        $scope.theForm.$setDirty();
+    }
+
+    function deleteState ($event, state) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if (state) {
+            var title = $translate.instant('dashboard.delete-state-title');
+            var content = $translate.instant('dashboard.delete-state-text', {stateName: state.name});
+            var confirm = $mdDialog.confirm()
+                .targetEvent($event)
+                .title(title)
+                .htmlContent(content)
+                .ariaLabel(title)
+                .cancel($translate.instant('action.no'))
+                .ok($translate.instant('action.yes'));
+
+            confirm._options.skipHide = true;
+            confirm._options.fullscreen = true;
+
+            $mdDialog.show(confirm).then(function () {
+                var index = getStateIndex(state.id);
+                if (index > -1) {
+                    vm.allStates.splice(index, 1);
+                }
+                $scope.theForm.$setDirty();
+                updateStates();
+            });
+
+
+        }
+    }
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function save() {
+        $scope.theForm.$setPristine();
+        var savedStates = {};
+        for (var i=0;i<vm.allStates.length;i++) {
+            var state = vm.allStates[i];
+            var id = state.id;
+            delete state.id;
+            savedStates[id] = state;
+        }
+        $mdDialog.hide(savedStates);
+    }
+}
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
new file mode 100644
index 0000000..9f86b9b
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
@@ -0,0 +1,127 @@
+<!--
+
+    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="{{ 'dashboard.manage-states' | translate }}" style="min-width: 600px;">
+    <form name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>{{ 'dashboard.manage-states' }}</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">
+                <fieldset ng-disabled="loading">
+                    <div class="manage-dashboard-states" layout="column">
+                        <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search === null">
+                            <div class="md-toolbar-tools">
+                                <span translate>dashboard.states</span>
+                                <span flex></span>
+                                <md-button class="md-icon-button" ng-click="vm.addState($event)">
+                                    <md-icon>add</md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'dashboard.add-state' | 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>
+                            </div>
+                        </md-toolbar>
+                        <md-toolbar class="md-table-toolbar md-default" ng-show="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">
+                                        {{ 'dashboard.search-states' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+                                <md-input-container flex>
+                                    <label>&nbsp;</label>
+                                    <input ng-model="vm.query.search" placeholder="{{ 'dashboard.search-states' | translate }}"/>
+                                </md-input-container>
+                                <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
+                                    <md-icon aria-label="Close" class="material-icons">close</md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.close' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+                            </div>
+                        </md-toolbar>
+                        <md-table-container>
+                            <table md-table>
+                                <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+                                    <tr md-row>
+                                        <th md-column md-order-by="name"><span translate>dashboard.state-name</span></th>
+                                        <th md-column md-order-by="id"><span translate>dashboard.state-id</span></th>
+                                        <th md-column md-order-by="root"><span translate>dashboard.is-root-state</span></th>
+                                        <th md-column><span>&nbsp</span></th>
+                                    </tr>
+                                </thead>
+                                <tbody md-body>
+                                    <tr md-row md-select="state" ng-disabled="state.root" md-select-id="id" md-auto-select ng-repeat="state in vm.states">
+                                        <td md-cell>{{state.name}}</td>
+                                        <td md-cell>{{state.id}}</td>
+                                        <td md-cell>
+                                            <md-checkbox aria-label="{{'dashboard.is-root-state' | translate }}"
+                                                         disabled ng-model="state.root">
+                                            </md-checkbox>
+                                        </td>
+                                        <td md-cell class="tb-action-cell">
+                                            <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
+                                                       ng-click="vm.editState($event, state)">
+                                                <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+                                                <md-tooltip md-direction="top">
+                                                    {{ 'dashboard.edit-state' | translate }}
+                                                </md-tooltip>
+                                            </md-button>
+                                            <md-button ng-show="!state.root" class="md-icon-button" aria-label="Delete" ng-click="vm.deleteState($event, state)">
+                                                <md-icon aria-label="Delete" class="material-icons">delete</md-icon>
+                                                <md-tooltip md-direction="top">
+                                                    {{ 'dashboard.delete-state' | 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.statesCount}}"
+                                             md-on-paginate="vm.onPaginate" md-page-select>
+                        </md-table-pagination>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+                {{ 'action.save' | 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/dashboard/states/select-target-state.controller.js b/ui/src/app/dashboard/states/select-target-state.controller.js
new file mode 100644
index 0000000..fa62eef
--- /dev/null
+++ b/ui/src/app/dashboard/states/select-target-state.controller.js
@@ -0,0 +1,35 @@
+/*
+ * 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 SelectTargetStateController($scope, $mdDialog, dashboardUtils, states) {
+
+    var vm = this;
+    vm.states = states;
+    vm.stateId = dashboardUtils.getRootStateId(vm.states);
+
+    vm.cancel = cancel;
+    vm.save = save;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function save() {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(vm.stateId);
+    }
+}
diff --git a/ui/src/app/dashboard/states/states-component.directive.js b/ui/src/app/dashboard/states/states-component.directive.js
new file mode 100644
index 0000000..fb5e77c
--- /dev/null
+++ b/ui/src/app/dashboard/states/states-component.directive.js
@@ -0,0 +1,117 @@
+/*
+ * 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 StatesComponent($compile, $templateCache, $controller, statesControllerService) {
+
+    var linker = function (scope, element) {
+
+        function destroyStateController() {
+            if (scope.statesController && angular.isFunction(scope.statesController.$onDestroy)) {
+                scope.statesController.$onDestroy();
+            }
+        }
+
+        function init() {
+
+            var stateController = scope.dashboardCtrl.dashboardCtx.stateController;
+
+            stateController.openState = function(id, params) {
+                if (scope.statesController) {
+                    scope.statesController.openState(id, params);
+                }
+            }
+
+            stateController.updateState = function(id, params) {
+                if (scope.statesController) {
+                    scope.statesController.updateState(id, params);
+                }
+            }
+
+            stateController.navigatePrevState = function(index) {
+                if (scope.statesController) {
+                    scope.statesController.navigatePrevState(index);
+                }
+            }
+
+            stateController.getStateId = function() {
+                if (scope.statesController) {
+                    return scope.statesController.getStateId();
+                } else {
+                    return '';
+                }
+            }
+
+            stateController.getStateParams = function() {
+                if (scope.statesController) {
+                    return scope.statesController.getStateParams();
+                } else {
+                    return {};
+                }
+            }
+        }
+
+        scope.$on('$destroy', function callOnDestroyHook() {
+            destroyStateController();
+        });
+
+        scope.$watch('scope.dashboardCtrl', function() {
+            if (scope.dashboardCtrl.dashboardCtx) {
+                init();
+            }
+        })
+
+        scope.$watch('statesControllerId', function(newValue) {
+            if (newValue) {
+                if (scope.statesController) {
+                    destroyStateController();
+                }
+                var statesControllerInfo = statesControllerService.getStateController(scope.statesControllerId);
+                if (!statesControllerInfo) {
+                    //fallback to default
+                    statesControllerInfo = statesControllerService.getStateController('default');
+                }
+                var template = $templateCache.get(statesControllerInfo.templateUrl);
+                element.html(template);
+                var locals = {};
+                angular.extend(locals, {$scope: scope, $element: element});
+                var controller = $controller(statesControllerInfo.controller, locals, true, 'vm');
+                controller.instance = controller();
+                scope.statesController = controller.instance;
+                scope.statesController.dashboardCtrl = scope.dashboardCtrl;
+                scope.statesController.states = scope.states;
+                $compile(element.contents())(scope);
+            }
+        });
+
+        scope.$watch('states', function() {
+            if (scope.statesController) {
+                scope.statesController.states = scope.states;
+            }
+        });
+
+    }
+
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            statesControllerId: '=',
+            dashboardCtrl: '=',
+            states: '='
+        }
+    };
+}
diff --git a/ui/src/app/dashboard/states/states-controller.service.js b/ui/src/app/dashboard/states/states-controller.service.js
new file mode 100644
index 0000000..e4c1f29
--- /dev/null
+++ b/ui/src/app/dashboard/states/states-controller.service.js
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import defaultStateControllerTemplate from './default-state-controller.tpl.html';
+import entityStateControllerTemplate from './entity-state-controller.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import DefaultStateController from './default-state-controller';
+import EntityStateController from './entity-state-controller';
+
+/*@ngInject*/
+export default function StatesControllerService() {
+
+    var statesControllers = {};
+    statesControllers['default'] = {
+        controller: DefaultStateController,
+        templateUrl: defaultStateControllerTemplate
+    };
+    statesControllers['entity'] = {
+        controller: EntityStateController,
+        templateUrl: entityStateControllerTemplate
+    };
+
+    var service = {
+        registerStatesController: registerStatesController,
+        getStateControllers: getStateControllers,
+        getStateController: getStateController
+    };
+
+    return service;
+
+    function registerStatesController(id, stateControllerInfo) {
+        statesControllers[id] = stateControllerInfo;
+    }
+
+    function getStateControllers() {
+        return statesControllers;
+    }
+
+    function getStateController(id) {
+        return statesControllers[id];
+    }
+
+}
diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js
index 181613b..3e8c9ac 100644
--- a/ui/src/app/device/device.controller.js
+++ b/ui/src/app/device/device.controller.js
@@ -48,12 +48,8 @@ export function DeviceCardController(types) {
 
 
 /*@ngInject*/
-export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
-}
-
-
-/*@ngInject*/
-export function DeviceController(userService, deviceService, customerService, $scope, $controller, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams,
+                                 $document, $mdDialog, $q, $translate, types) {
 
     var customerId = $stateParams.customerId;
 
@@ -136,8 +132,8 @@ export function DeviceController(userService, deviceService, customerService, $s
         }
 
         if (vm.devicesScope === 'tenant') {
-            fetchDevicesFunction = function (pageLink) {
-                return deviceService.getTenantDevices(pageLink, true);
+            fetchDevicesFunction = function (pageLink, deviceType) {
+                return deviceService.getTenantDevices(pageLink, true, null, deviceType);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.deleteDevice(deviceId);
@@ -247,8 +243,8 @@ export function DeviceController(userService, deviceService, customerService, $s
 
 
         } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
-            fetchDevicesFunction = function (pageLink) {
-                return deviceService.getCustomerDevices(customerId, pageLink, true);
+            fetchDevicesFunction = function (pageLink, deviceType) {
+                return deviceService.getCustomerDevices(customerId, pageLink, true, null, deviceType);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.unassignDeviceFromCustomer(deviceId);
@@ -373,6 +369,7 @@ export function DeviceController(userService, deviceService, customerService, $s
         var deferred = $q.defer();
         deviceService.saveDevice(device).then(
             function success(savedDevice) {
+                $rootScope.$broadcast('deviceSaved');
                 var devices = [ savedDevice ];
                 customerService.applyAssignedCustomersInfo(devices).then(
                     function success(items) {
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index eb50a15..eda4fb2 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -25,6 +25,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
         var template = $templateCache.get(deviceFieldsetTemplate);
         element.html(template);
 
+        scope.types = types;
         scope.isAssignedToCustomer = false;
         scope.isPublic = false;
         scope.assignedCustomer = null;
diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js
index c1f5b27..b1ca7ed 100644
--- a/ui/src/app/device/device.routes.js
+++ b/ui/src/app/device/device.routes.js
@@ -20,7 +20,7 @@ import devicesTemplate from './devices.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DeviceRoutes($stateProvider) {
+export default function DeviceRoutes($stateProvider, types) {
     $stateProvider
         .state('home.devices', {
             url: '/devices',
@@ -37,6 +37,8 @@ export default function DeviceRoutes($stateProvider) {
             data: {
                 devicesType: 'tenant',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.device,
                 pageTitle: 'device.devices'
             },
             ncyBreadcrumb: {
@@ -58,6 +60,8 @@ export default function DeviceRoutes($stateProvider) {
             data: {
                 devicesType: 'customer',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.device,
                 pageTitle: 'customer.devices'
             },
             ncyBreadcrumb: {
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index bb84af7..58f4c85 100644
--- a/ui/src/app/device/device-card.tpl.html
+++ b/ui/src/app/device/device-card.tpl.html
@@ -15,5 +15,8 @@
     limitations under the License.
 
 -->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
-<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
+<div flex layout="column" style="margin-top: -10px;">
+    <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+    <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+    <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
+</div>
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index fd6c9d1..767e47a 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -66,6 +66,13 @@
 	      		<div translate ng-message="required">device.name-required</div>
 	    	</div>				
 		</md-input-container>
+        <tb-entity-subtype-autocomplete
+                ng-disabled="loading || !isEdit"
+                tb-required="true"
+                the-form="theForm"
+                ng-model="device.type"
+                entity-type="types.entityType.device">
+        </tb-entity-subtype-autocomplete>
         <md-input-container class="md-block">
             <md-checkbox ng-disabled="loading || !isEdit" flex aria-label="{{ 'device.is-gateway' | translate }}"
                          ng-model="device.additionalInfo.gateway">{{ 'device.is-gateway' | translate }}
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
index 0e07cb4..e5822a4 100644
--- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -13,8 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import selectTargetStateTemplate from '../../dashboard/states/select-target-state.tpl.html';
+import selectTargetLayoutTemplate from '../../dashboard/layouts/select-target-layout.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
 /*@ngInject*/
-export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
+export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, $q, $document, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
 
     var vm = this;
 
@@ -31,22 +39,87 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
         $mdDialog.cancel();
     }
 
-    function add() {
-        $scope.theForm.$setPristine();
+    function selectTargetState($event, dashboard) {
+        var deferred = $q.defer();
+        var states = dashboard.configuration.states;
+        var stateIds = Object.keys(states);
+        if (stateIds.length > 1) {
+            $mdDialog.show({
+                controller: 'SelectTargetStateController',
+                controllerAs: 'vm',
+                templateUrl: selectTargetStateTemplate,
+                parent: angular.element($document[0].body),
+                locals: {
+                    states: states
+                },
+                fullscreen: true,
+                skipHide: true,
+                targetEvent: $event
+            }).then(
+                function success(stateId) {
+                    deferred.resolve(stateId);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+
+        } else {
+            deferred.resolve(stateIds[0]);
+        }
+        return deferred.promise;
+    }
+
+    function selectTargetLayout($event, dashboard, targetState) {
+        var deferred = $q.defer();
+        var layouts = dashboard.configuration.states[targetState].layouts;
+        var layoutIds = Object.keys(layouts);
+        if (layoutIds.length > 1) {
+            $mdDialog.show({
+                controller: 'SelectTargetLayoutController',
+                controllerAs: 'vm',
+                templateUrl: selectTargetLayoutTemplate,
+                parent: angular.element($document[0].body),
+                fullscreen: true,
+                skipHide: true,
+                targetEvent: $event
+            }).then(
+                function success(layoutId) {
+                    deferred.resolve(layoutId);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        } else {
+            deferred.resolve(layoutIds[0]);
+        }
+        return deferred.promise;
+    }
+
+    function add($event) {
         if (vm.addToDashboardType === 0) {
             dashboardService.getDashboard(vm.dashboardId).then(
                 function success(dashboard) {
-                    addWidgetToDashboard(dashboard);
+                    selectTargetState($event, dashboard).then(
+                        function(targetState) {
+                            selectTargetLayout($event, dashboard, targetState).then(
+                                function(targetLayout) {
+                                    addWidgetToDashboard(dashboard, targetState, targetLayout);
+                                }
+                            );
+                       }
+                    );
                 },
                 function fail() {}
             );
         } else {
-            addWidgetToDashboard(vm.newDashboard);
+            addWidgetToDashboard(vm.newDashboard, 'default', 'main');
         }
 
     }
 
-    function addWidgetToDashboard(theDashboard) {
+    function addWidgetToDashboard(theDashboard, targetState, targetLayout) {
         var aliasesInfo = {
             datasourceAliases: {},
             targetDeviceAliases: {}
@@ -60,13 +133,25 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
                 entityList: [entityId]
             }
         };
-        theDashboard = itembuffer.addWidgetToDashboard(theDashboard, vm.widget, aliasesInfo, null, 48, -1, -1);
-        dashboardService.saveDashboard(theDashboard).then(
-            function success(dashboard) {
-                $mdDialog.hide();
-                if (vm.openDashboard) {
-                    $state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id});
-                }
+        itembuffer.addWidgetToDashboard(theDashboard, targetState, targetLayout, vm.widget, aliasesInfo, null, 48, null, -1, -1).then(
+            function(theDashboard) {
+                dashboardService.saveDashboard(theDashboard).then(
+                    function success(dashboard) {
+                        $scope.theForm.$setPristine();
+                        $mdDialog.hide();
+                        if (vm.openDashboard) {
+                            var stateParams = {
+                                dashboardId: dashboard.id.id
+                            }
+                            var stateIds = Object.keys(dashboard.configuration.states);
+                            var stateIndex = stateIds.indexOf(targetState);
+                            if (stateIndex > 0) {
+                                stateParams.state = angular.toJson([ {id: targetState, params: {}} ]);
+                            }
+                            $state.go('home.dashboards.dashboard', stateParams);
+                        }
+                    }
+                );
             }
         );
     }
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
index eb16077..d7fe890 100644
--- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-dialog aria-label="{{ 'attribute.add-widget-to-dashboard' | translate }}" style="min-width: 400px;">
-    <form name="theForm" ng-submit="vm.add()">
+    <form name="theForm" ng-submit="vm.add($event)">
         <md-toolbar>
             <div class="md-toolbar-tools">
                 <h2 translate>attribute.add-widget-to-dashboard</h2>
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
index bf3b8cd..1afd1bb 100644
--- a/ui/src/app/entity/attribute/attribute-table.tpl.html
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -63,7 +63,7 @@
                         {{ 'action.search' | translate }}
                     </md-tooltip>
                 </md-button>
-                <md-input-container md-theme="tb-search-input" flex>
+                <md-input-container flex>
                     <label>&nbsp;</label>
                     <input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
                 </md-input-container>
@@ -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.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..705c6b8
--- /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 = {};
+
+        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.entityType = value.entityType;
+                scope.model.entityId = value.id;
+            } else {
+                scope.model.entityType = null;
+                scope.model.entityId = 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.tpl.html b/ui/src/app/entity/entity-select.tpl.html
new file mode 100644
index 0000000..ce0a16b
--- /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 ng-if="model.entityType"
+                            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-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
new file mode 100644
index 0000000..98110b0
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
@@ -0,0 +1,141 @@
+/*
+ * 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-subtype-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySubtypeAutocompleteTemplate from './entity-subtype-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entitySubtypeAutocompleteTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.subType = null;
+        scope.subTypeSearchText = '';
+        scope.entitySubtypes = null;
+
+        scope.fetchSubTypes = function(searchText) {
+            var deferred = $q.defer();
+            loadSubTypes().then(
+                function success(subTypes) {
+                    var result = $filter('filter')(subTypes, {'$': searchText});
+                    if (result && result.length) {
+                        deferred.resolve(result);
+                    } else {
+                        deferred.resolve([searchText]);
+                    }
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+            return deferred.promise;
+        }
+
+        scope.subTypeSearchTextChanged = function() {
+        }
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                ngModelCtrl.$setViewValue(scope.subType);
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            scope.subType = ngModelCtrl.$viewValue;
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('subType', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+        function loadSubTypes() {
+            var deferred = $q.defer();
+            if (!scope.entitySubtypes) {
+                var entitySubtypesPromise;
+                if (scope.entityType == types.entityType.asset) {
+                    entitySubtypesPromise = assetService.getAssetTypes();
+                } else if (scope.entityType == types.entityType.device) {
+                    entitySubtypesPromise = deviceService.getDeviceTypes();
+                }
+                if (entitySubtypesPromise) {
+                    entitySubtypesPromise.then(
+                        function success(types) {
+                            scope.entitySubtypes = [];
+                            types.forEach(function (type) {
+                                scope.entitySubtypes.push(type.type);
+                            });
+                            deferred.resolve(scope.entitySubtypes);
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                } else {
+                    deferred.reject();
+                }
+            } else {
+                deferred.resolve(scope.entitySubtypes);
+            }
+            return deferred.promise;
+        }
+
+        function load() {
+            if (scope.entityType == types.entityType.asset) {
+                scope.selectEntitySubtypeText = 'asset.select-asset-type';
+                scope.entitySubtypeText = 'asset.asset-type';
+                scope.entitySubtypeRequiredText = 'asset.asset-type-required';
+            } else if (scope.entityType == types.entityType.device) {
+                scope.selectEntitySubtypeText = 'device.select-device-type';
+                scope.entitySubtypeText = 'device.device-type';
+                scope.entitySubtypeRequiredText = 'device.device-type-required';
+                scope.$on('deviceSaved', function() {
+                    scope.entitySubtypes = null;
+                });
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled',
+            entityType: "="
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.tpl.html b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
new file mode 100644
index 0000000..ce220ee
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
@@ -0,0 +1,41 @@
+<!--
+
+    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="subType"
+                 ng-model="subType"
+                 md-selected-item="subType"
+                 md-search-text="subTypeSearchText"
+                 md-search-text-change="subTypeSearchTextChanged()"
+                 md-items="item in fetchSubTypes(subTypeSearchText)"
+                 md-item-text="item"
+                 md-min-length="0"
+                 placeholder="{{ selectEntitySubtypeText | translate }}"
+                 md-floating-label="{{ entitySubtypeText | translate }}"
+                 md-select-on-match="true"
+                 md-menu-class="tb-entity-subtype-autocomplete">
+    <md-item-template>
+        <div class="tb-entity-subtype-item">
+            <span md-highlight-text="subTypeSearchText" md-highlight-flags="^i">{{item}}</span>
+        </div>
+    </md-item-template>
+    <div ng-messages="theForm.subType.$error">
+        <div translate ng-message="required">{{ entitySubtypeRequiredText }}</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/entity/entity-subtype-select.directive.js b/ui/src/app/entity/entity-subtype-select.directive.js
new file mode 100644
index 0000000..36d9729
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-select.directive.js
@@ -0,0 +1,138 @@
+/*
+ * 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-subtype-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySubtypeSelectTemplate from './entity-subtype-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySubtypeSelect($compile, $templateCache, $translate, assetService, deviceService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entitySubtypeSelectTemplate);
+        element.html(template);
+
+        if (angular.isDefined(attrs.hideLabel)) {
+            scope.showLabel = false;
+        } else {
+            scope.showLabel = true;
+        }
+
+        scope.ngModelCtrl = ngModelCtrl;
+
+        scope.entitySubtypes = [];
+
+        scope.subTypeName = function(subType) {
+            if (subType && subType.length) {
+                if (scope.typeTranslatePrefix) {
+                    return $translate.instant(scope.typeTranslatePrefix + '.' + subType);
+                } else {
+                    return subType;
+                }
+            } else {
+                return $translate.instant('entity.all-subtypes');
+            }
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('entitySubtype', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.updateView = function () {
+            ngModelCtrl.$setViewValue(scope.entitySubtype);
+        };
+
+        ngModelCtrl.$render = function () {
+            scope.entitySubtype = ngModelCtrl.$viewValue;
+        };
+
+        function loadSubTypes() {
+            scope.entitySubtypes = [];
+            var entitySubtypesPromise;
+            if (scope.entityType == types.entityType.asset) {
+                entitySubtypesPromise = assetService.getAssetTypes();
+            } else if (scope.entityType == types.entityType.device) {
+                entitySubtypesPromise = deviceService.getDeviceTypes();
+            }
+            if (entitySubtypesPromise) {
+                entitySubtypesPromise.then(
+                    function success(types) {
+                        scope.entitySubtypes.push('');
+                        types.forEach(function(type) {
+                            scope.entitySubtypes.push(type.type);
+                        });
+                        if (scope.entitySubtypes.indexOf(scope.entitySubtype) == -1) {
+                            scope.entitySubtype = '';
+                        }
+                    },
+                    function fail() {}
+                );
+            }
+
+        }
+
+        function load() {
+            if (scope.entityType == types.entityType.asset) {
+                scope.entitySubtypeTitle = 'asset.asset-type';
+                scope.entitySubtypeRequiredText = 'asset.asset-type-required';
+            } else if (scope.entityType == types.entityType.device) {
+                scope.entitySubtypeTitle = 'device.device-type';
+                scope.entitySubtypeRequiredText = 'device.device-type-required';
+            }
+            scope.entitySubtypes.length = 0;
+            if (scope.entitySubtypesList && scope.entitySubtypesList.length) {
+                scope.entitySubtypesList.forEach(function(subType) {
+                    scope.entitySubtypes.push(subType);
+                });
+            } else {
+                loadSubTypes();
+                if (scope.entityType == types.entityType.asset) {
+                    scope.$on('assetSaved', function() {
+                        loadSubTypes();
+                    });
+                } else if (scope.entityType == types.entityType.device) {
+                    scope.$on('deviceSaved', function() {
+                        loadSubTypes();
+                    });
+                }
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            entityType: "=",
+            entitySubtypesList: "=?",
+            typeTranslatePrefix: "@?"
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-subtype-select.scss b/ui/src/app/entity/entity-subtype-select.scss
new file mode 100644
index 0000000..a6b2fe1
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-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.
+ */
+
+md-select.tb-entity-subtype-select {
+  min-width: 200px;
+}
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 07c0862..bed9562 100644
--- a/ui/src/app/entity/index.js
+++ b/ui/src/app/entity/index.js
@@ -16,12 +16,17 @@
 
 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)
@@ -29,7 +34,12 @@ export default angular.module('thingsboard.entity', [])
     .controller('AddAttributeDialogController', AddAttributeDialogController)
     .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
     .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/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.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/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js
index 86cd676..57bbcd3 100644
--- a/ui/src/app/global-interceptor.service.js
+++ b/ui/src/app/global-interceptor.service.js
@@ -174,7 +174,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
             }
         }
 
-        if (unhandled) {
+        if (unhandled && !ignoreErrors) {
             if (rejection.data && !rejection.data.message) {
                 getToast().showError(rejection.data);
             } else if (rejection.data && rejection.data.message) {
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 4b89a99..af04687 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -332,8 +332,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
 
     // Widget functions
 
-    function exportWidget(dashboard, widget) {
-        var widgetItem = itembuffer.prepareWidgetItem(dashboard, widget);
+    function exportWidget(dashboard, sourceState, sourceLayout, widget) {
+        var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
         var name = widgetItem.widget.config.title;
         name = name.toLowerCase().replace(/\W/g,"_");
         exportToPc(prepareExport(widgetItem), name + '.json');
@@ -355,6 +355,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                 }
             }
         }
+        return aliasesInfo;
     }
 
     function prepareEntityAlias(aliasInfo) {
@@ -379,21 +380,24 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
             entityType = aliasInfo.entityType;
         }
         return {
-            alias: aliasInfo.aliasName,
+            aliasName: aliasInfo.aliasName,
             entityType: entityType,
             entityFilter: entityFilter
         };
     }
 
-    function importWidget($event, dashboard, onAliasesUpdate) {
+    function importWidget($event, dashboard, targetState, targetLayoutFunction, onAliasesUpdateFunction) {
+        var deferred = $q.defer();
         openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
             function success(widgetItem) {
                 if (!validateImportedWidget(widgetItem)) {
                     toast.showError($translate.instant('dashboard.invalid-widget-file-error'));
+                    deferred.reject();
                 } else {
                     var widget = widgetItem.widget;
                     var aliasesInfo = prepareAliasesInfo(widgetItem.aliasesInfo);
                     var originalColumns = widgetItem.originalColumns;
+                    var originalSize = widgetItem.originalSize;
 
                     var datasourceAliases = aliasesInfo.datasourceAliases;
                     var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
@@ -439,25 +443,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                                                         targetDeviceAliases[datasourceIndex].entityFilter = entityAlias.entityFilter;
                                                     }
                                                 }
-                                                addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+                                                addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+                                                    aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
                                             },
-                                            function fail() {}
+                                            function fail() {
+                                                deferred.reject();
+                                            }
                                         );
                                     } else {
-                                        addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+                                        addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+                                            aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
                                     }
                                 }
                             );
                         } else {
-                            addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+                            addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+                                aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
                         }
                     } else {
-                        addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+                        addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+                            aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
                     }
                 }
             },
-            function fail() {}
+            function fail() {
+                deferred.reject();
+            }
         );
+        return deferred.promise;
     }
 
     function validateImportedWidget(widgetItem) {
@@ -476,8 +489,26 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
-    function addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns) {
-        itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, -1, -1);
+    function addImportedWidget(dashboard, targetState, targetLayoutFunction, event, widget,
+                               aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred) {
+        targetLayoutFunction(event).then(
+            function success(targetLayout) {
+                itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget,
+                    aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then(
+                        function() {
+                            deferred.resolve(
+                                {
+                                    widget: widget,
+                                    layoutId: targetLayout
+                                }
+                            );
+                        }
+                );
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
     }
 
     // Dashboard functions
diff --git a/ui/src/app/layout/home.controller.js b/ui/src/app/layout/home.controller.js
index 4979501..5091da8 100644
--- a/ui/src/app/layout/home.controller.js
+++ b/ui/src/app/layout/home.controller.js
@@ -25,7 +25,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
 /* eslint-disable angular/angularelement */
 
 /*@ngInject*/
-export default function HomeController(loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
+export default function HomeController(types, loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
                                        $window, $log, $mdMedia, $animate, $timeout) {
 
     var siteSideNav = $('.tb-site-sidenav', $element);
@@ -38,8 +38,11 @@ export default function HomeController(loginService, userService, deviceService,
     if (angular.isUndefined($rootScope.searchConfig)) {
         $rootScope.searchConfig = {
             searchEnabled: false,
+            searchByEntitySubtype: false,
+            searchEntityType: null,
             showSearch: false,
-            searchText: ""
+            searchText: "",
+            searchEntitySubtype: ""
         };
     }
 
@@ -47,6 +50,7 @@ export default function HomeController(loginService, userService, deviceService,
     vm.isLockSidenav = false;
 
     vm.displaySearchMode = displaySearchMode;
+    vm.displayEntitySubtypeSearch = displayEntitySubtypeSearch;
     vm.openSidenav = openSidenav;
     vm.goBack = goBack;
     vm.searchTextUpdated = searchTextUpdated;
@@ -54,25 +58,35 @@ export default function HomeController(loginService, userService, deviceService,
     vm.toggleFullscreen = toggleFullscreen;
 
     $scope.$on('$stateChangeSuccess', function (evt, to, toParams, from) {
+        watchEntitySubtype(false);
         if (angular.isDefined(to.data.searchEnabled)) {
             $scope.searchConfig.searchEnabled = to.data.searchEnabled;
+            $scope.searchConfig.searchByEntitySubtype = to.data.searchByEntitySubtype;
+            $scope.searchConfig.searchEntityType = to.data.searchEntityType;
             if ($scope.searchConfig.searchEnabled === false || to.name !== from.name) {
                 $scope.searchConfig.showSearch = false;
                 $scope.searchConfig.searchText = "";
+                $scope.searchConfig.searchEntitySubtype = "";
             }
         } else {
             $scope.searchConfig.searchEnabled = false;
+            $scope.searchConfig.searchByEntitySubtype = false;
+            $scope.searchConfig.searchEntityType = null;
             $scope.searchConfig.showSearch = false;
             $scope.searchConfig.searchText = "";
+            $scope.searchConfig.searchEntitySubtype = "";
         }
+        watchEntitySubtype($scope.searchConfig.searchByEntitySubtype);
     });
 
-    if ($mdMedia('gt-sm')) {
+    vm.isGtSm = $mdMedia('gt-sm');
+    if (vm.isGtSm) {
         vm.isLockSidenav = true;
         $animate.enabled(siteSideNav, false);
     }
 
     $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) {
+        vm.isGtSm = isGtSm;
         vm.isLockSidenav = isGtSm;
         vm.isShowSidenav = isGtSm;
         if (!isGtSm) {
@@ -84,11 +98,28 @@ export default function HomeController(loginService, userService, deviceService,
         }
     });
 
+    function watchEntitySubtype(enableWatch) {
+        if ($scope.entitySubtypeWatch) {
+            $scope.entitySubtypeWatch();
+        }
+        if (enableWatch) {
+            $scope.entitySubtypeWatch = $scope.$watch('searchConfig.searchEntitySubtype', function (newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    $scope.$broadcast('searchEntitySubtypeUpdated');
+                }
+            });
+        }
+    }
+
     function displaySearchMode() {
         return $scope.searchConfig.searchEnabled &&
             $scope.searchConfig.showSearch;
     }
 
+    function displayEntitySubtypeSearch() {
+        return $scope.searchConfig.searchByEntitySubtype && vm.isGtSm;
+    }
+
     function toggleFullscreen() {
         if (Fullscreen.isEnabled()) {
             Fullscreen.cancel();
diff --git a/ui/src/app/layout/home.scss b/ui/src/app/layout/home.scss
index edcdbc8..a809cf6 100644
--- a/ui/src/app/layout/home.scss
+++ b/ui/src/app/layout/home.scss
@@ -70,3 +70,11 @@ md-icon.tb-logo-title {
   z-index: 2;
   white-space: nowrap;
 }
+
+.tb-entity-subtype-search {
+  margin-top: 15px;
+}
+
+.tb-entity-search {
+  margin-top: 34px;
+}
diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html
index bfb37eb..8e642eb 100644
--- a/ui/src/app/layout/home.tpl.html
+++ b/ui/src/app/layout/home.tpl.html
@@ -39,7 +39,7 @@
   </md-sidenav>
 
   <div flex layout="column" tabIndex="-1" role="main">
-    <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
+    <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar">
     	<div layout="row" flex class="md-toolbar-tools">
 		      <md-button id="main" hide-gt-sm ng-show="!forceFullscreen"
 		      		class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
@@ -55,10 +55,18 @@
 			  <div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools">
 				  <span ng-cloak ncy-breadcrumb></span>
 			  </div>
-			  <md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>
-	              <label>&nbsp;</label>
-	              <input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
-	          </md-input-container>		      
+			  <div layout="row" ng-show="vm.displaySearchMode()" md-theme="tb-dark" flex>
+				  <div class="tb-entity-subtype-search" layout="row" layout-align="start center" ng-if="vm.displayEntitySubtypeSearch()">
+					  <tb-entity-subtype-select
+							  entity-type="searchConfig.searchEntityType"
+							  ng-model="searchConfig.searchEntitySubtype">
+					  </tb-entity-subtype-select>
+				  </div>
+				  <md-input-container ng-class="{'tb-entity-search': vm.displayEntitySubtypeSearch()}" flex>
+					  <label>&nbsp;</label>
+					  <input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
+				  </md-input-container>
+			  </div>
 		      <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}" ng-show="searchConfig.searchEnabled" ng-click="searchConfig.showSearch = !searchConfig.showSearch">
 		      	  <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
           	  </md-button>
diff --git a/ui/src/app/layout/user-menu.directive.js b/ui/src/app/layout/user-menu.directive.js
index 6d09e67..53e8c45 100644
--- a/ui/src/app/layout/user-menu.directive.js
+++ b/ui/src/app/layout/user-menu.directive.js
@@ -48,16 +48,10 @@ function UserMenuController($scope, userService, $translate, $state) {
     var dashboardUser = userService.getCurrentUser();
 
     vm.authorityName = authorityName;
-    vm.displaySearchMode = displaySearchMode;
     vm.logout = logout;
     vm.openProfile = openProfile;
     vm.userDisplayName = userDisplayName;
 
-    function displaySearchMode() {
-        return $scope.searchConfig.searchEnabled &&
-            $scope.searchConfig.showSearch;
-    }
-
     function authorityName() {
         var name = "user.anonymous";
         if (dashboardUser) {
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 1fc45f7..d25db5d 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -14,7 +14,10 @@
  * limitations under the License.
  */
 
+import ThingsboardMissingTranslateHandler from './translate-handler';
+
 export default angular.module('thingsboard.locale', [])
+    .factory('tbMissingTranslationHandler', ThingsboardMissingTranslateHandler)
     .constant('locales',
         {
             'en_US': {
@@ -62,6 +65,8 @@ export default angular.module('thingsboard.locale', [])
                     "undo": "Undo",
                     "copy": "Copy",
                     "paste": "Paste",
+                    "copy-reference": "Copy reference",
+                    "paste-reference": "Paste reference",
                     "import": "Import",
                     "export": "Export",
                     "share-via": "Share via {{provider}}"
@@ -101,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",
@@ -119,6 +130,9 @@ export default angular.module('thingsboard.locale', [])
                     "unassign-from-customer": "Unassign from customer",
                     "delete": "Delete asset",
                     "asset-public": "Asset is public",
+                    "asset-type": "Asset type",
+                    "asset-type-required": "Asset type is required.",
+                    "select-asset-type": "Select asset type",
                     "name": "Name",
                     "name-required": "Name is required.",
                     "description": "Description",
@@ -149,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",
@@ -161,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.",
@@ -202,6 +220,7 @@ export default angular.module('thingsboard.locale', [])
                     "enter-search": "Enter search"
                 },
                 "customer": {
+                    "customer": "Customer",
                     "customers": "Customers",
                     "management": "Customer management",
                     "dashboard": "Customer Dashboard",
@@ -238,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",
@@ -296,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",
@@ -323,7 +345,9 @@ export default angular.module('thingsboard.locale', [])
                     "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
                     "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
                     "display-title": "Display dashboard title",
+                    "toolbar-always-open": "Keep toolbar opened",
                     "title-color": "Title color",
+                    "display-dashboards-selection": "Display dashboards selection",
                     "display-entities-selection": "Display entities selection",
                     "display-dashboard-timewindow": "Display timewindow",
                     "display-dashboard-export": "Display export",
@@ -350,13 +374,37 @@ export default angular.module('thingsboard.locale', [])
                     "public": "Public",
                     "public-link": "Public link",
                     "copy-public-link": "Copy public link",
-                    "public-link-copied-message": "Dashboard public link has been copied to clipboard"
+                    "public-link-copied-message": "Dashboard public link has been copied to clipboard",
+                    "manage-states": "Manage dashboard states",
+                    "states": "Dashboard states",
+                    "search-states": "Search dashboard states",
+                    "selected-states": "{ count, select, 1 {1 dashboard state} other {# dashboard states} } selected",
+                    "edit-state": "Edit dashboard state",
+                    "delete-state": "Delete dashboard state",
+                    "add-state": "Add dashboard state",
+                    "state": "Dashboard state",
+                    "state-name": "Name",
+                    "state-name-required": "Dashboard state name is required.",
+                    "state-name-exists": "Dashboard state with the same name is already exists.",
+                    "state-id": "State Id",
+                    "state-id-required": "Dashboard state id is required.",
+                    "state-id-exists": "Dashboard state with the same id is already exists.",
+                    "invalid-state-id-format": "Only alphanumeric characters and underscore are allowed.",
+                    "is-root-state": "Root state",
+                    "delete-state-title": "Delete dashboard state",
+                    "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?",
+                    "show-details": "Show details",
+                    "hide-details": "Hide details",
+                    "select-state": "Select target state",
+                    "state-controller": "State controller"
                 },
                 "datakey": {
                     "settings": "Settings",
                     "advanced": "Advanced",
                     "label": "Label",
                     "color": "Color",
+                    "units": "Special symbol to show next to value",
+                    "decimals": "Number of digits after floating point",
                     "data-generation-func": "Data generation function",
                     "use-data-post-processing-func": "Use data post-processing function",
                     "configuration": "Data key configuration",
@@ -391,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",
@@ -446,6 +494,9 @@ export default angular.module('thingsboard.locale', [])
                     "rsa-key-required": "RSA public key is required.",
                     "secret": "Secret",
                     "secret-required": "Secret is required.",
+                    "device-type": "Device type",
+                    "device-type-required": "Device type is required.",
+                    "select-device-type": "Select device type",
                     "name": "Name",
                     "name-required": "Name is required.",
                     "description": "Description",
@@ -460,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"
@@ -490,6 +542,7 @@ export default angular.module('thingsboard.locale', [])
                     "entity-list-empty": "No entities selected.",
                     "entity-name-filter-required": "Entity name filter is required.",
                     "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
+                    "all-subtypes": "All",
                     "type": "Type",
                     "type-device": "Device",
                     "type-asset": "Asset",
@@ -497,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.",
@@ -569,6 +625,15 @@ export default angular.module('thingsboard.locale', [])
                     "no-return-error": "Function must return value!",
                     "return-type-mismatch": "Function must return value of '{{type}}' type!"
                 },
+                "layout": {
+                    "layout": "Layout",
+                    "manage": "Manage layouts",
+                    "settings": "Layout settings",
+                    "color": "Color",
+                    "main": "Main",
+                    "right": "Right",
+                    "select": "Select target layout"
+                },
                 "legend": {
                     "position": "Legend position",
                     "show-max": "Show max value",
@@ -625,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",
@@ -638,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",
@@ -650,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",
@@ -702,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",
@@ -728,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} }",
@@ -756,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",
@@ -781,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 56e4d57..3246070 100644
--- a/ui/src/app/locale/locale.constant-zh.js
+++ b/ui/src/app/locale/locale.constant-zh.js
@@ -116,12 +116,12 @@ export default function addLocaleChinese(locales) {
             "delete-attributes-text" : "注意,确认后所有选中的属性都会被删除。",
             "delete-attributes" : "删除属性",
             "enter-attribute-value" : "输入属性值",
-            "show-on-widget" : "在小部件上显示",
-            "widget-mode" : "小部件模式",
-            "next-widget" : "下一个小部件",
-            "prev-widget" : "上一个小部件",
+            "show-on-widget" : "在部件上显示",
+            "widget-mode" : "部件模式",
+            "next-widget" : "下一个部件",
+            "prev-widget" : "上一个部件",
             "add-to-dashboard" : "添加到仪表板",
-            "add-widget-to-dashboard" : "将小部件添加到仪表板",
+            "add-widget-to-dashboard" : "将部件添加到仪表板",
             "selected-attributes" : "{ count, select, 1 {1 attribute} other {# attributes} } 被选中",
             "selected-telemetry" : "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } 被选中"
         },
@@ -169,7 +169,7 @@ export default function addLocaleChinese(locales) {
             "customer-details" : "客户详情",
             "delete-customer-title" : "您确定要删除客户'{{customerTitle}}'吗?",
             "delete-customer-text" : "小心!确认后,客户及其所有相关数据将不可恢复。",
-            "delete-customers-title" : "您确定要删除 { count, select, 1 {1 customer} other {# customers} }?吗",
+            "delete-customers-title" : "您确定要删除 { count, select, 1 {1 customer} other {# customers} }吗?",
             "delete-customers-action-title" : "删除 { count, select, 1 {1 customer} other {# customers} }",
             "delete-customers-text" : "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。",
             "manage-users" : "管理用户",
@@ -199,11 +199,11 @@ export default function addLocaleChinese(locales) {
             "make-public" : "使仪表板公有",
             "make-private" : "使仪表板私有",
             "no-dashboards-text" : "没有找到仪表板",
-            "no-widgets" : "没有配置小部件",
-            "add-widget" : "添加新的小部件",
+            "no-widgets" : "没有配置部件",
+            "add-widget" : "添加新的部件",
             "title" : "标题",
-            "select-widget-title" : "选择小部件",
-            "select-widget-subtitle" : "可用的小部件类型列表",
+            "select-widget-title" : "选择部件",
+            "select-widget-subtitle" : "可用的部件类型列表",
             "delete" : "删除仪表板",
             "title-required" : "需要标题。",
             "description" : "描述",
@@ -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" : "创建新的仪表板",
@@ -273,11 +273,11 @@ export default function addLocaleChinese(locales) {
             "dashboard-file" : "仪表板文件",
             "invalid-dashboard-file-error" : "无法导入仪表板: 仪表板数据结构无效。",
             "dashboard-import-missing-aliases-title" : "配置导入仪表板使用的别名",
-            "create-new-widget" : "创建新小部件",
-            "import-widget" : "导入小部件",
-            "widget-file" : "小部件文件",
-            "invalid-widget-file-error" : "无法导入窗口小部件: 窗口小部件数据结构无效。",
-            "widget-import-missing-aliases-title" : "配置导入的窗口小部件使用的别名",
+            "create-new-widget" : "创建新部件",
+            "import-widget" : "导入部件",
+            "widget-file" : "部件文件",
+            "invalid-widget-file-error" : "无法导入窗口部件: 窗口部件数据结构无效。",
+            "widget-import-missing-aliases-title" : "配置导入的窗口部件使用的别名",
             "open-toolbar" : "打开仪表板工具栏",
             "close-toolbar" : "关闭工具栏",
             "configuration-error" : "配置错误",
@@ -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" : "事件",
@@ -694,34 +694,34 @@ export default function addLocaleChinese(locales) {
             "true" : "真"
         },
         "widget" : {
-            "widget-library" : "小部件库",
-            "widget-bundle" : "小部件包",
-            "select-widgets-bundle" : "选择小部件包",
-            "management" : "小部件管理",
-            "editor" : "小部件编辑器",
-            "widget-type-not-found" : "加载小部件配置时出现问题。<br> 可能关联的\n 小部件类型已删除。",
-            "widget-type-load-error" : "由于以下错误,小工具未加载:",
-            "remove" : "删除小部件",
-            "edit" : "编辑小部件",
-            "remove-widget-title" : "您确定要删除小部件 '{{widgetTitle}}' 吗?",
-            "remove-widget-text" : "确认后,窗口小部件和所有相关数据将不可恢复。",
+            "widget-library" : "部件库",
+            "widget-bundle" : "部件包",
+            "select-widgets-bundle" : "选择部件包",
+            "management" : "部件管理",
+            "editor" : "部件编辑器",
+            "widget-type-not-found" : "加载部件配置时出现问题。<br> 可能关联的\n 部件类型已删除。",
+            "widget-type-load-error" : "由于以下错误,部件未加载:",
+            "remove" : "删除部件",
+            "edit" : "编辑部件",
+            "remove-widget-title" : "您确定要删除部件 '{{widgetTitle}}' 吗?",
+            "remove-widget-text" : "确认后,窗口部件和所有相关数据将不可恢复。",
             "timeseries" : "时间序列",
             "latest-values" : "最新值",
-            "rpc" : "控件小部件",
-            "static" : "静态小部件",
-            "select-widget-type" : "选择窗口小部件类型",
-            "missing-widget-title-error" : "小部件标题必须指定!",
-            "widget-saved" : "小部件已保存",
-            "unable-to-save-widget-error" : "无法保存窗口小部件! 小部件有错误!",
-            "save" : "保存小部件",
-            "saveAs" : "将小部件另存为",
-            "save-widget-type-as" : "将小部件类型另存为",
-            "save-widget-type-as-text" : "请输入新的小部件标题和/或选择目标小部件包",
+            "rpc" : "控件部件",
+            "static" : "静态部件",
+            "select-widget-type" : "选择窗口部件类型",
+            "missing-widget-title-error" : "部件标题必须指定!",
+            "widget-saved" : "部件已保存",
+            "unable-to-save-widget-error" : "无法保存窗口部件! 部件有错误!",
+            "save" : "保存部件",
+            "saveAs" : "将部件另存为",
+            "save-widget-type-as" : "将部件类型另存为",
+            "save-widget-type-as-text" : "请输入新的部件标题和/或选择目标部件包",
             "toggle-fullscreen" : "切换全屏",
-            "run" : "运行小部件",
-            "title" : "小部件标题",
-            "title-required" : "需要小部件标题。",
-            "type" : "小部件类型",
+            "run" : "运行部件",
+            "title" : "部件标题",
+            "title-required" : "需要部件标题。",
+            "type" : "部件类型",
             "resources" : "资源",
             "resource-url" : "JavaScript/CSS URL",
             "remove-resource" : "删除资源",
@@ -732,42 +732,42 @@ export default function addLocaleChinese(locales) {
             "settings-schema" : "设置模式",
             "datakey-settings-schema" : "数据键设置模式",
             "javascript" : "Javascript",
-            "remove-widget-type-title" : "您确定要删除小部件类型 '{{widgetName}}'吗?",
-            "remove-widget-type-text" : "确认后,窗口小部件类型和所有相关数据将不可恢复。",
-            "remove-widget-type" : "删除小部件类型",
-            "add-widget-type" : "添加新的小部件类型",
-            "widget-type-load-failed-error" : "无法加载小部件类型!",
-            "widget-template-load-failed-error" : "无法加载小部件模板!",
-            "add" : "添加小部件",
-            "undo" : "撤消小部件更改",
-            "export" : "导出小部件"
+            "remove-widget-type-title" : "您确定要删除部件类型 '{{widgetName}}'吗?",
+            "remove-widget-type-text" : "确认后,窗口部件类型和所有相关数据将不可恢复。",
+            "remove-widget-type" : "删除部件类型",
+            "add-widget-type" : "添加新的部件类型",
+            "widget-type-load-failed-error" : "无法加载部件类型!",
+            "widget-template-load-failed-error" : "无法加载部件模板!",
+            "add" : "添加部件",
+            "undo" : "撤消部件更改",
+            "export" : "导出部件"
         },
         "widgets-bundle" : {
             "current" : "当前包",
-            "widgets-bundles" : "小部件包",
-            "add" : "添加小部件包",
-            "delete" : "删除小部件包",
+            "widgets-bundles" : "部件包",
+            "add" : "添加部件包",
+            "delete" : "删除部件包",
             "title" : "标题",
             "title-required" : "标题是必填项。",
-            "add-widgets-bundle-text" : "添加新的小部件包",
-            "no-widgets-bundles-text" : "找不到小部件包",
-            "empty" : "小部件包是空的",
+            "add-widgets-bundle-text" : "添加新的部件包",
+            "no-widgets-bundles-text" : "找不到部件包",
+            "empty" : "部件包是空的",
             "details" : "详情",
-            "widgets-bundle-details" : "小部件包详细信息",
-            "delete-widgets-bundle-title" : "您确定要删除小部件包 '{{widgetsBundleTitle}}'吗?",
-            "delete-widgets-bundle-text" : "小心!确认后,小部件包和所有相关数据将不可恢复。",
+            "widgets-bundle-details" : "部件包详细信息",
+            "delete-widgets-bundle-title" : "您确定要删除部件包 '{{widgetsBundleTitle}}'吗?",
+            "delete-widgets-bundle-text" : "小心!确认后,部件包和所有相关数据将不可恢复。",
             "delete-widgets-bundles-title" : "你确定你要删除 { count, select, 1 {1 widgets bundle} other {# widgets bundles} } 吗?",
             "delete-widgets-bundles-action-title" : "删除  { count, select, 1 {1 widgets bundle} other {# widgets bundles} }",
-            "delete-widgets-bundles-text" : "小心!确认后,所有选定的小部件包将被删除,所有相关数据将不可恢复。",
-            "no-widgets-bundles-matching" : "没有找到与 '{{widgetsBundle}}' 匹配的小部件包。",
-            "widgets-bundle-required" : "需要小部件包。",
+            "delete-widgets-bundles-text" : "小心!确认后,所有选定的部件包将被删除,所有相关数据将不可恢复。",
+            "no-widgets-bundles-matching" : "没有找到与 '{{widgetsBundle}}' 匹配的部件包。",
+            "widgets-bundle-required" : "需要部件包。",
             "system" : "系统",
-            "import" : "导入小部件包",
-            "export" : "导出小部件包",
-            "export-failed-error" : "无法导出小部件包: {{error}}",
-            "create-new-widgets-bundle" : "创建新的小部件包",
-            "widgets-bundle-file" : "小部件包文件",
-            "invalid-widgets-bundle-file-error" : "无法导入小部件包:无效的小部件包数据结构。"
+            "import" : "导入部件包",
+            "export" : "导出部件包",
+            "export-failed-error" : "无法导出部件包: {{error}}",
+            "create-new-widgets-bundle" : "创建新的部件包",
+            "widgets-bundle-file" : "部件包文件",
+            "invalid-widgets-bundle-file-error" : "无法导入部件包:无效的部件包数据结构。"
         },
         "widget-config" : {
             "data" : "数据",
@@ -798,12 +798,12 @@ export default function addLocaleChinese(locales) {
             "target-device" : "目标设备"
         },
         "widget-type" : {
-            "import" : "导入小部件类型",
-            "export" : "导出小部件类型",
-            "export-failed-error" : "无法导出小部件类型: {{error}}",
-            "create-new-widget-type" : "创建新的小部件类型",
-            "widget-type-file" : "小部件类型文件",
-            "invalid-widget-type-file-error" : "无法导入小部件类型:无效的小部件类型数据结构。"
+            "import" : "导入部件类型",
+            "export" : "导出部件类型",
+            "export-failed-error" : "无法导出部件类型: {{error}}",
+            "create-new-widget-type" : "创建新的部件类型",
+            "widget-type-file" : "部件类型文件",
+            "invalid-widget-type-file-error" : "无法导入部件类型:无效的部件类型数据结构。"
         },
         "language" : {
             "language" : "语言",
@@ -811,10 +811,10 @@ export default function addLocaleChinese(locales) {
             "ko_KR" : "韩语",
             "zh_CN" : "汉语",
             "ru_RU" : "俄语",
-            "es_ES": "西班牙語"
+            "es_ES": "西班牙语"
         }
     };
     angular.extend(locales, {
         'zh_CN' : zh_CN
     });
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/profile/profile.controller.js b/ui/src/app/profile/profile.controller.js
index cea51be..187a9d2 100644
--- a/ui/src/app/profile/profile.controller.js
+++ b/ui/src/app/profile/profile.controller.js
@@ -31,7 +31,7 @@ export default function ProfileController(userService, $scope, $document, $mdDia
         en_US: {value : "en_US", name: "language.en_US"}, 
         ko_KR: {value : "ko_KR", name: "language.ko_KR"},
         zh_CN: {value : "zh_CN", name: "language.zh_CN"},
-        ru_RU: {value : "ru_RU", name: "language.ru_RU"}ñ
+        ru_RU: {value : "ru_RU", name: "language.ru_RU"},
         es_ES: {value : "es_ES", name: "language.es_ES"},
     };
 
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 4b54b4d..afb63bf 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,15 +24,19 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
     .name;
 
 /*@ngInject*/
-function ItemBuffer(bufferStore, types, dashboardUtils) {
+function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
 
     const WIDGET_ITEM = "widget_item";
+    const WIDGET_REFERENCE = "widget_reference";
 
     var service = {
         prepareWidgetItem: prepareWidgetItem,
         copyWidget: copyWidget,
+        copyWidgetReference: copyWidgetReference,
         hasWidget: hasWidget,
+        canPasteWidgetReference: canPasteWidgetReference,
         pasteWidget: pasteWidget,
+        pasteWidgetReference: pasteWidgetReference,
         addWidgetToDashboard: addWidgetToDashboard
     }
 
@@ -66,16 +70,42 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
         };
     }
 
-    function prepareWidgetItem(dashboard, widget) {
+    function getOriginalColumns(dashboard, sourceState, sourceLayout) {
+        var originalColumns = 24;
+        var gridSettings = null;
+        var state = dashboard.configuration.states[sourceState];
+        var layoutCount = Object.keys(state.layouts).length;
+        if (state) {
+            var layout = state.layouts[sourceLayout];
+            if (layout) {
+                gridSettings = layout.gridSettings;
+
+            }
+        }
+        if (gridSettings &&
+            gridSettings.columns) {
+            originalColumns = gridSettings.columns;
+        }
+        originalColumns = originalColumns * layoutCount;
+        return originalColumns;
+    }
+
+    function getOriginalSize(dashboard, sourceState, sourceLayout, widget) {
+        var layout = dashboard.configuration.states[sourceState].layouts[sourceLayout];
+        var widgetLayout = layout.widgets[widget.id];
+        return {
+            sizeX: widgetLayout.sizeX,
+            sizeY: widgetLayout.sizeY
+        }
+    }
+
+    function prepareWidgetItem(dashboard, sourceState, sourceLayout, widget) {
         var aliasesInfo = {
             datasourceAliases: {},
             targetDeviceAliases: {}
         };
-        var originalColumns = 24;
-        if (dashboard.configuration.gridSettings &&
-            dashboard.configuration.gridSettings.columns) {
-            originalColumns = dashboard.configuration.gridSettings.columns;
-        }
+        var originalColumns = getOriginalColumns(dashboard, sourceState, sourceLayout);
+        var originalSize = getOriginalSize(dashboard, sourceState, sourceLayout, widget);
         if (widget.config && dashboard.configuration
             && dashboard.configuration.entityAliases) {
             var entityAlias;
@@ -105,37 +135,113 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
         return {
             widget: widget,
             aliasesInfo: aliasesInfo,
+            originalSize: originalSize,
             originalColumns: originalColumns
-        }
+        };
     }
 
-    function copyWidget(dashboard, widget) {
-        var widgetItem = prepareWidgetItem(dashboard, widget);
+    function prepareWidgetReference(dashboard, sourceState, sourceLayout, widget) {
+        var originalColumns = getOriginalColumns(dashboard, sourceState, sourceLayout);
+        var originalSize = getOriginalSize(dashboard, sourceState, sourceLayout, widget);
+
+        return {
+            dashboardId: dashboard.id.id,
+            sourceState: sourceState,
+            sourceLayout: sourceLayout,
+            widgetId: widget.id,
+            originalSize: originalSize,
+            originalColumns: originalColumns
+        };
+    }
+
+    function copyWidget(dashboard, sourceState, sourceLayout, widget) {
+        var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
         bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
     }
 
+    function copyWidgetReference(dashboard, sourceState, sourceLayout, widget) {
+        var widgetReference = prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
+        bufferStore.set(WIDGET_REFERENCE, angular.toJson(widgetReference));
+    }
+
     function hasWidget() {
         return bufferStore.get(WIDGET_ITEM);
     }
 
-    function pasteWidget(targetDashboard, position, onAliasesUpdate) {
+    function canPasteWidgetReference(dashboard, state, layout) {
+        var widgetReferenceJson = bufferStore.get(WIDGET_REFERENCE);
+        if (widgetReferenceJson) {
+            var widgetReference = angular.fromJson(widgetReferenceJson);
+            if (widgetReference.dashboardId === dashboard.id.id) {
+                if ((widgetReference.sourceState != state || widgetReference.sourceLayout != layout)
+                    && dashboard.configuration.widgets[widgetReference.widgetId]) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    function pasteWidgetReference(targetDashboard, targetState, targetLayout, position) {
+        var deferred = $q.defer();
+        var widgetReferenceJson = bufferStore.get(WIDGET_REFERENCE);
+        if (widgetReferenceJson) {
+            var widgetReference = angular.fromJson(widgetReferenceJson);
+            var widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
+            if (widget) {
+                var originalColumns = widgetReference.originalColumns;
+                var originalSize = widgetReference.originalSize;
+                var targetRow = -1;
+                var targetColumn = -1;
+                if (position) {
+                    targetRow = position.row;
+                    targetColumn = position.column;
+                }
+                addWidgetToDashboard(targetDashboard, targetState, targetLayout, widget, null,
+                    null, originalColumns, originalSize, targetRow, targetColumn).then(
+                    function () {
+                        deferred.resolve(widget);
+                    }
+                );
+            } else {
+                deferred.reject();
+            }
+        } else {
+            deferred.reject();
+        }
+        return deferred.promise;
+    }
+
+    function pasteWidget(targetDashboard, targetState, targetLayout, position, onAliasesUpdateFunction) {
+        var deferred = $q.defer();
         var widgetItemJson = bufferStore.get(WIDGET_ITEM);
         if (widgetItemJson) {
             var widgetItem = angular.fromJson(widgetItemJson);
             var widget = widgetItem.widget;
             var aliasesInfo = widgetItem.aliasesInfo;
             var originalColumns = widgetItem.originalColumns;
+            var originalSize = widgetItem.originalSize;
             var targetRow = -1;
             var targetColumn = -1;
             if (position) {
                 targetRow = position.row;
                 targetColumn = position.column;
             }
-            addWidgetToDashboard(targetDashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, targetRow, targetColumn);
+            widget.id = utils.guid();
+            addWidgetToDashboard(targetDashboard, targetState, targetLayout, widget, aliasesInfo,
+                onAliasesUpdateFunction, originalColumns, originalSize, targetRow, targetColumn).then(
+                    function () {
+                        deferred.resolve(widget);
+                    }
+            );
+        } else {
+            deferred.reject();
         }
+        return deferred.promise;
     }
 
-    function addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, row, column) {
+    function addWidgetToDashboard(dashboard, targetState, targetLayout, widget, aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, row, column) {
+        var deferred = $q.defer();
         var theDashboard;
         if (dashboard) {
             theDashboard = dashboard;
@@ -145,42 +251,28 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
 
         theDashboard = dashboardUtils.validateAndUpdateDashboard(theDashboard);
 
-        var newEntityAliases = updateAliases(theDashboard, widget, aliasesInfo);
-
-        var targetColumns = 24;
-        if (theDashboard.configuration.gridSettings &&
-            theDashboard.configuration.gridSettings.columns) {
-            targetColumns = theDashboard.configuration.gridSettings.columns;
-        }
-        if (targetColumns != originalColumns) {
-            var ratio = targetColumns / originalColumns;
-            widget.sizeX *= ratio;
-            widget.sizeY *= ratio;
+        var callAliasUpdateFunction = false;
+        if (aliasesInfo) {
+            var newEntityAliases = updateAliases(theDashboard, widget, aliasesInfo);
+            var aliasesUpdated = !angular.equals(newEntityAliases, theDashboard.configuration.entityAliases);
+            if (aliasesUpdated) {
+                theDashboard.configuration.entityAliases = newEntityAliases;
+                if (onAliasesUpdateFunction) {
+                    callAliasUpdateFunction = true;
+                }
+            }
         }
-        if (row > -1 && column > - 1) {
-            widget.row = row;
-            widget.col = column;
+        dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column);
+        if (callAliasUpdateFunction) {
+            onAliasesUpdateFunction().then(
+                function() {
+                    deferred.resolve(theDashboard);
+                }
+            );
         } else {
-            row = 0;
-            for (var w in theDashboard.configuration.widgets) {
-                var existingWidget = theDashboard.configuration.widgets[w];
-                var wRow = existingWidget.row ? existingWidget.row : 0;
-                var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
-                var bottom = wRow + wSizeY;
-                row = Math.max(row, bottom);
-            }
-            widget.row = row;
-            widget.col = 0;
-        }
-        var aliasesUpdated = !angular.equals(newEntityAliases, theDashboard.configuration.entityAliases);
-        if (aliasesUpdated) {
-            theDashboard.configuration.entityAliases = newEntityAliases;
-            if (onAliasesUpdate) {
-                onAliasesUpdate();
-            }
+            deferred.resolve(theDashboard);
         }
-        theDashboard.configuration.widgets.push(widget);
-        return theDashboard;
+        return deferred.promise;
     }
 
     function updateAliases(dashboard, widget, aliasesInfo) {
@@ -242,6 +334,4 @@ function ItemBuffer(bufferStore, types, dashboardUtils) {
         }
         return newAlias;
     }
-
-
 }
\ No newline at end of file
diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js
index eeba2f4..4e702bd 100644
--- a/ui/src/app/user/user.controller.js
+++ b/ui/src/app/user/user.controller.js
@@ -22,7 +22,7 @@ import userCard from './user-card.tpl.html';
 
 
 /*@ngInject*/
-export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate) {
+export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate, types) {
 
     var tenantId = $stateParams.tenantId;
     var customerId = $stateParams.customerId;
@@ -87,7 +87,10 @@ export default function UserController(userService, toast, $scope, $controller, 
             };
             saveUserFunction = function (user) {
                 user.authority = "TENANT_ADMIN";
-                user.tenantId = {id: tenantId};
+                user.tenantId = {
+                    entityType: types.entityType.tenant,
+                    id: tenantId
+                };
                 return userService.saveUser(user);
             };
             refreshUsersParamsFunction = function () {
@@ -100,7 +103,10 @@ export default function UserController(userService, toast, $scope, $controller, 
             };
             saveUserFunction = function (user) {
                 user.authority = "CUSTOMER_USER";
-                user.customerId = {id: customerId};
+                user.customerId = {
+                    entityType: types.entityType.customer,
+                    id: customerId
+                };
                 return userService.saveUser(user);
             };
             refreshUsersParamsFunction = function () {
diff --git a/ui/src/app/user/user.directive.js b/ui/src/app/user/user.directive.js
index 1a4b069..bd78dc2 100644
--- a/ui/src/app/user/user.directive.js
+++ b/ui/src/app/user/user.directive.js
@@ -28,6 +28,10 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi
         var template = $templateCache.get(userFieldsetTemplate);
         element.html(template);
 
+        scope.isTenantAdmin = function() {
+            return scope.user && scope.user.authority === 'TENANT_ADMIN';
+        }
+
         scope.isCustomerUser = function() {
             return scope.user && scope.user.authority === 'CUSTOMER_USER';
         }
diff --git a/ui/src/app/user/user-fieldset.tpl.html b/ui/src/app/user/user-fieldset.tpl.html
index 7c9b0d6..a2559ff 100644
--- a/ui/src/app/user/user-fieldset.tpl.html
+++ b/ui/src/app/user/user-fieldset.tpl.html
@@ -43,10 +43,19 @@
             <label translate>user.description</label>
             <textarea ng-model="user.additionalInfo.description" rows="2"></textarea>
         </md-input-container>
-        <section class="tb-default-dashboard" flex layout="column" ng-show="isCustomerUser()">
+        <section class="tb-default-dashboard" flex layout="column">
             <span class="tb-default-dashboard-label" ng-class="{'tb-disabled-label': loading || !isEdit}" translate>user.default-dashboard</span>
             <section flex layout="column" layout-gt-sm="row">
-                <tb-dashboard-autocomplete flex
+                <tb-dashboard-autocomplete ng-if="isTenantAdmin()"
+                                           flex
+                                           ng-disabled="loading || !isEdit"
+                                           the-form="theForm"
+                                           ng-model="user.additionalInfo.defaultDashboardId"
+                                           tenant-id="user.tenantId.id"
+                                           select-first-dashboard="false">
+                </tb-dashboard-autocomplete>
+                <tb-dashboard-autocomplete ng-if="isCustomerUser()"
+                                     flex
                                      ng-disabled="loading || !isEdit"
                                      the-form="theForm"
                                      ng-model="user.additionalInfo.defaultDashboardId"
diff --git a/ui/src/app/widget/lib/analogue-linear-gauge.js b/ui/src/app/widget/lib/analogue-linear-gauge.js
index 1a7c99a..a045f94 100644
--- a/ui/src/app/widget/lib/analogue-linear-gauge.js
+++ b/ui/src/app/widget/lib/analogue-linear-gauge.js
@@ -41,8 +41,7 @@ export default class TbAnalogueLinearGauge {
 
         var valueInt = settings.valueInt || 3;
 
-        var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
-            ? settings.valueDec : ctx.decimals;
+        var valueDec = getValueDec(settings);
 
         step = parseFloat(parseFloat(step).toFixed(valueDec));
 
@@ -74,6 +73,32 @@ export default class TbAnalogueLinearGauge {
         var progressColorStart = tinycolor(keyColor).setAlpha(0.05).toRgbString();
         var progressColorEnd = tinycolor(keyColor).darken().toRgbString();
 
+        function getUnits(settings) {
+            var dataKey;
+            if (ctx.data && ctx.data[0]) {
+                dataKey = ctx.data[0].dataKey;
+            }
+            if (dataKey && dataKey.units && dataKey.units.length) {
+                return dataKey.units;
+            } else {
+                return angular.isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units;
+            }
+        }
+
+        function getValueDec(settings) {
+            var dataKey;
+            if (ctx.data && ctx.data[0]) {
+                dataKey = ctx.data[0].dataKey;
+            }
+            if (dataKey && angular.isDefined(dataKey.decimals)) {
+                return dataKey.decimals;
+            } else {
+                return (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+                    ? settings.valueDec : ctx.decimals;
+            }
+        }
+
+
         function getFontFamily(fontSettings) {
             var family = fontSettings && fontSettings.family ? fontSettings.family : 'Roboto';
             if (family === 'RobotoDraft') {
@@ -92,7 +117,7 @@ export default class TbAnalogueLinearGauge {
             maxValue: maxValue,
             majorTicks: majorTicks,
             minorTicks: settings.minorTicks || 2,
-            units: angular.isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units,
+            units: getUnits(settings),
             title: ((settings.showUnitTitle !== false) ?
                 (settings.unitTitle && settings.unitTitle.length > 0 ?
                     settings.unitTitle : dataKey.label) : ''),
diff --git a/ui/src/app/widget/lib/analogue-radial-gauge.js b/ui/src/app/widget/lib/analogue-radial-gauge.js
index 3a526bd..76a2c38 100644
--- a/ui/src/app/widget/lib/analogue-radial-gauge.js
+++ b/ui/src/app/widget/lib/analogue-radial-gauge.js
@@ -42,8 +42,7 @@ export default class TbAnalogueRadialGauge {
 
         var valueInt = settings.valueInt || 3;
 
-        var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
-            ? settings.valueDec : ctx.decimals;
+        var valueDec = getValueDec(settings);
 
         step = parseFloat(parseFloat(step).toFixed(valueDec));
 
@@ -71,6 +70,31 @@ export default class TbAnalogueRadialGauge {
 
         var colorNumbers = tinycolor(keyColor).darken(20).toRgbString();
 
+        function getUnits(settings) {
+            var dataKey;
+            if (ctx.data && ctx.data[0]) {
+                dataKey = ctx.data[0].dataKey;
+            }
+            if (dataKey && dataKey.units && dataKey.units.length) {
+                return dataKey.units;
+            } else {
+                return angular.isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units;
+            }
+        }
+
+        function getValueDec(settings) {
+            var dataKey;
+            if (ctx.data && ctx.data[0]) {
+                dataKey = ctx.data[0].dataKey;
+            }
+            if (dataKey && angular.isDefined(dataKey.decimals)) {
+                return dataKey.decimals;
+            } else {
+                return (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+                    ? settings.valueDec : ctx.decimals;
+            }
+        }
+
         function getFontFamily(fontSettings) {
             var family = fontSettings && fontSettings.family ? fontSettings.family : 'Roboto';
             if (family === 'RobotoDraft') {
@@ -89,7 +113,7 @@ export default class TbAnalogueRadialGauge {
             maxValue: maxValue,
             majorTicks: majorTicks,
             minorTicks: settings.minorTicks || 2,
-            units: angular.isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units,
+            units: getUnits(settings),
             title: ((settings.showUnitTitle !== false) ?
                 (settings.unitTitle && settings.unitTitle.length > 0 ?
                     settings.unitTitle : dataKey.label) : ''),
diff --git a/ui/src/app/widget/lib/canvas-digital-gauge.js b/ui/src/app/widget/lib/canvas-digital-gauge.js
index 33283ef..1290f5f 100644
--- a/ui/src/app/widget/lib/canvas-digital-gauge.js
+++ b/ui/src/app/widget/lib/canvas-digital-gauge.js
@@ -54,10 +54,13 @@ export default class TbCanvasDigitalGauge {
             this.localSettings.levelColors = settings.levelColors.slice();
         }
 
-        this.localSettings.decimals = (angular.isDefined(settings.decimals) && settings.decimals !== null)
-            ? settings.decimals : ctx.decimals;
+        this.localSettings.decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals :
+            ((angular.isDefined(settings.decimals) && settings.decimals !== null)
+            ? settings.decimals : ctx.decimals);
+
+        this.localSettings.units = dataKey.units && dataKey.units.length ? dataKey.units :
+            (angular.isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units);
 
-        this.localSettings.units = angular.isDefined(settings.units) && settings.units.length > 0 ? settings.units : ctx.units;
         this.localSettings.hideValue = settings.showValue !== true;
         this.localSettings.hideMinMax = settings.showMinMax !== true;
 
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js
index 340b7eb..6687f1a 100644
--- a/ui/src/app/widget/lib/flot-widget.js
+++ b/ui/src/app/widget/lib/flot-widget.js
@@ -110,8 +110,10 @@ export default class TbFlot {
 
         if (this.chartType === 'pie') {
             ctx.tooltipFormatter = function(item) {
+                var units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : tbFlot.ctx.trackUnits;
+                var decimals = angular.isDefined(item.series.dataKey.decimals) ? item.series.dataKey.decimals : tbFlot.ctx.trackDecimals;
                 var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
-                    item.datapoint[1][0][1], tbFlot.ctx.trackUnits, tbFlot.ctx.trackDecimals, true, item.series.percent);
+                    item.datapoint[1][0][1], units, decimals, true, item.series.percent);
                 return divElement.prop('outerHTML');
             };
         } else {
@@ -133,18 +135,19 @@ export default class TbFlot {
                     if (tbFlot.ctx.tooltipIndividual && seriesHoverInfo.index !== seriesIndex) {
                         continue;
                     }
+                    var units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : tbFlot.ctx.trackUnits;
+                    var decimals = angular.isDefined(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : tbFlot.ctx.trackDecimals;
                     var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color,
-                        seriesHoverInfo.value, tbFlot.ctx.trackUnits, tbFlot.ctx.trackDecimals, seriesHoverInfo.index === seriesIndex);
+                        seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex);
                     content += divElement.prop('outerHTML');
                 }
                 return content;
             };
         }
 
-        ctx.trackDecimals = angular.isDefined(settings.decimals) ?
-            settings.decimals : ctx.decimals;
+        ctx.trackDecimals = ctx.decimals;
 
-        ctx.trackUnits = angular.isDefined(settings.units) ? settings.units : ctx.units;
+        ctx.trackUnits = ctx.units;
 
         ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false);
         ctx.tooltipCumulative = angular.isDefined(settings.tooltipCumulative) ? settings.tooltipCumulative : false;
@@ -178,7 +181,7 @@ export default class TbFlot {
                 font: angular.copy(font),
                 labelFont: angular.copy(font)
             };
-            options.yaxis = {
+            this.yaxis = {
                 font: angular.copy(font),
                 labelFont: angular.copy(font)
             };
@@ -194,32 +197,33 @@ export default class TbFlot {
                 options.xaxis.labelFont.size = options.xaxis.font.size+2;
                 options.xaxis.labelFont.weight = "bold";
             }
-            if (settings.yaxis) {
+
+            ctx.yAxisTickFormatter = function(value/*, axis*/) {
                 if (settings.yaxis.showLabels === false) {
-                    options.yaxis.tickFormatter = function() {
-                        return '';
-                    };
-                } else if (ctx.trackUnits && ctx.trackUnits.length > 0) {
-                    options.yaxis.tickFormatter = function(value, axis) {
-                        var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1,
-                            formatted = "" + Math.round(value * factor) / factor;
-                        if (axis.tickDecimals != null) {
-                            var decimal = formatted.indexOf("."),
-                                precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
-
-                            if (precision < axis.tickDecimals) {
-                                formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision);
-                            }
-                        }
-                        formatted += ' ' + tbFlot.ctx.trackUnits;
-                        return formatted;
-                    };
+                    return '';
+                }
+                var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1,
+                    formatted = "" + Math.round(value * factor) / factor;
+                if (this.tickDecimals != null) {
+                    var decimal = formatted.indexOf("."),
+                        precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
+
+                    if (precision < this.tickDecimals) {
+                        formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision);
+                    }
                 }
-                options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color;
-                options.yaxis.label = settings.yaxis.title || null;
-                options.yaxis.labelFont.color = options.yaxis.font.color;
-                options.yaxis.labelFont.size = options.yaxis.font.size+2;
-                options.yaxis.labelFont.weight = "bold";
+                formatted += ' ' + this.tickUnits;
+                return formatted;
+            }
+
+            this.yaxis.tickFormatter = ctx.yAxisTickFormatter;
+
+            if (settings.yaxis) {
+                this.yaxis.font.color = settings.yaxis.color || this.yaxis.font.color;
+                this.yaxis.label = settings.yaxis.title || null;
+                this.yaxis.labelFont.color = this.yaxis.font.color;
+                this.yaxis.labelFont.size = this.yaxis.font.size+2;
+                this.yaxis.labelFont.weight = "bold";
             }
 
             options.grid.borderWidth = 1;
@@ -235,7 +239,7 @@ export default class TbFlot {
                     options.xaxis.tickLength = 0;
                 }
                 if (settings.grid.horizontalLines === false) {
-                    options.yaxis.tickLength = 0;
+                    this.yaxis.tickLength = 0;
                 }
             }
 
@@ -317,6 +321,8 @@ export default class TbFlot {
         this.subscription = subscription;
         this.$element = $element;
         var colors = [];
+        this.yaxes = [];
+        var yaxesMap = {};
         for (var i = 0; i < this.subscription.data.length; i++) {
             var series = this.subscription.data[i];
             colors.push(series.dataKey.color);
@@ -348,8 +354,29 @@ export default class TbFlot {
 
             series.highlightColor = lineColor.toRgbString();
 
+            if (this.yaxis) {
+                var units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.ctx.trackUnits;
+                var yaxis;
+                if (keySettings.showSeparateAxis) {
+                    yaxis = this.createYAxis(keySettings, units);
+                    this.yaxes.push(yaxis);
+                } else {
+                    yaxis = yaxesMap[units];
+                    if (!yaxis) {
+                        yaxis = this.createYAxis(keySettings, units);
+                        yaxesMap[units] = yaxis;
+                        this.yaxes.push(yaxis);
+                    }
+                }
+                series.yaxisIndex = this.yaxes.indexOf(yaxis);
+                series.yaxis = series.yaxisIndex+1;
+                yaxis.keysInfo[i] = {hidden: false};
+                yaxis.hidden = false;
+            }
         }
+
         this.options.colors = colors;
+        this.options.yaxes = angular.copy(this.yaxes);
         if (this.chartType === 'line' || this.chartType === 'bar') {
             if (this.chartType === 'bar') {
                 this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
@@ -379,6 +406,23 @@ export default class TbFlot {
         }
     }
 
+    createYAxis(keySettings, units) {
+        var yaxis = angular.copy(this.yaxis);
+
+        var label = keySettings.axisTitle && keySettings.axisTitle.length ? keySettings.axisTitle : yaxis.label;
+        var tickDecimals = angular.isDefined(keySettings.axisTickDecimals) ? keySettings.axisTickDecimals : 0;
+        var position = keySettings.axisPosition && keySettings.axisPosition.length ? keySettings.axisPosition : "left";
+
+        yaxis.label = label;
+        yaxis.tickUnits = units;
+        yaxis.tickDecimals = tickDecimals;
+        yaxis.alignTicksWithAxis = position == "right" ? 1 : null;
+        yaxis.position = position;
+
+        yaxis.keysInfo = [];
+        return yaxis;
+    }
+
     update() {
         if (this.updateTimeoutHandle) {
             this.ctx.$scope.$timeout.cancel(this.updateTimeoutHandle);
@@ -387,17 +431,64 @@ export default class TbFlot {
         if (this.subscription) {
             if (!this.isMouseInteraction && this.ctx.plot) {
                 if (this.chartType === 'line' || this.chartType === 'bar') {
+
+                    var axisVisibilityChanged = false;
+                    if (this.yaxis) {
+                        for (var i = 0; i < this.subscription.data.length; i++) {
+                            var series = this.subscription.data[i];
+                            var yaxisIndex = series.yaxisIndex;
+                            if (this.yaxes[yaxisIndex].keysInfo[i].hidden != series.dataKey.hidden) {
+                                this.yaxes[yaxisIndex].keysInfo[i].hidden = series.dataKey.hidden;
+                                axisVisibilityChanged = true;
+                            }
+                        }
+                        if (axisVisibilityChanged) {
+                            this.options.yaxes.length = 0;
+                            for (var y = 0; y < this.yaxes.length; y++) {
+                                var yaxis = this.yaxes[y];
+                                var hidden = true;
+                                for (var k = 0; k < yaxis.keysInfo.length; k++) {
+                                    if (yaxis.keysInfo[k]) {
+                                        hidden = hidden && yaxis.keysInfo[k].hidden;
+                                    }
+                                }
+                                yaxis.hidden = hidden
+                                var newIndex = -1;
+                                if (!yaxis.hidden) {
+                                    this.options.yaxes.push(yaxis);
+                                    newIndex = this.options.yaxes.length;
+                                }
+                                for (k = 0; k < yaxis.keysInfo.length; k++) {
+                                    if (yaxis.keysInfo[k]) {
+                                        this.subscription.data[k].yaxis = newIndex;
+                                    }
+                                }
+
+                            }
+                            this.options.yaxis = {
+                                show: this.options.yaxes.length ? true : false
+                            };
+                        }
+                    }
+
                     this.options.xaxis.min = this.subscription.timeWindow.minTime;
                     this.options.xaxis.max = this.subscription.timeWindow.maxTime;
-                    this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
-                    this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
                     if (this.chartType === 'bar') {
                         this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
-                        this.ctx.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
                     }
-                    this.ctx.plot.setData(this.subscription.data);
-                    this.ctx.plot.setupGrid();
-                    this.ctx.plot.draw();
+
+                    if (axisVisibilityChanged) {
+                        this.redrawPlot();
+                    } else {
+                        this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
+                        this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
+                        if (this.chartType === 'bar') {
+                            this.ctx.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
+                        }
+                        this.ctx.plot.setData(this.subscription.data);
+                        this.ctx.plot.setupGrid();
+                        this.ctx.plot.draw();
+                    }
                 } else if (this.chartType === 'pie') {
                     if (this.ctx.animatedPie) {
                         this.nextPieDataAnimation(true);
@@ -726,6 +817,26 @@ export default class TbFlot {
                         "title": "Show points",
                             "type": "boolean",
                             "default": false
+                    },
+                    "showSeparateAxis": {
+                        "title": "Show separate axis",
+                        "type": "boolean",
+                        "default": false
+                    },
+                    "axisTitle": {
+                        "title": "Axis title",
+                        "type": "string",
+                        "default": ""
+                    },
+                    "axisTickDecimals": {
+                        "title": "Axis tick number of digits after floating point",
+                        "type": "number",
+                        "default": 0
+                    },
+                    "axisPosition": {
+                        "title": "Axis position",
+                        "type": "string",
+                        "default": "left"
                     }
                 },
                 "required": ["showLines", "fillLines", "showPoints"]
@@ -733,7 +844,26 @@ export default class TbFlot {
                 "form": [
                 "showLines",
                 "fillLines",
-                "showPoints"
+                "showPoints",
+                "showSeparateAxis",
+                "axisTitle",
+                "axisTickDecimals",
+                {
+                    "key": "axisPosition",
+                    "type": "rc-select",
+                    "multiple": false,
+                    "items": [
+                        {
+                            "value": "left",
+                            "label": "Left"
+                        },
+                        {
+                            "value": "right",
+                            "label": "Right"
+                        }
+                    ]
+                }
+
             ]
         }
     }
@@ -760,6 +890,17 @@ export default class TbFlot {
         }
     }
 
+    redrawPlot() {
+        if (this.ctx.plot) {
+            this.ctx.plot.destroy();
+            if (this.chartType === 'pie' && this.ctx.animatedPie) {
+                this.ctx.plot = $.plot(this.$element, this.pieData, this.options);
+            } else {
+                this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options);
+            }
+        }
+    }
+
     destroy() {
         if (this.ctx.plot) {
             this.ctx.plot.destroy();
@@ -974,6 +1115,8 @@ export default class TbFlot {
                     hoverIndex: hoverIndex,
                     color: series.dataKey.color,
                     label: series.dataKey.label,
+                    units: series.dataKey.units,
+                    decimals: series.dataKey.decimals,
                     time: pointTime,
                     distance: hoverDistance,
                     index: i
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js
index 69fb85b..bda8835 100644
--- a/ui/src/app/widget/widget-library.controller.js
+++ b/ui/src/app/widget/widget-library.controller.js
@@ -87,7 +87,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
                                     var sizeX = 8;
                                     var sizeY = Math.floor(widgetTypeInfo.sizeY);
                                     var widget = {
-                                        id: widgetType.id,
+                                        typeId: widgetType.id,
                                         isSystemType: isSystem,
                                         bundleAlias: bundleAlias,
                                         typeAlias: widgetTypeInfo.alias,
@@ -158,7 +158,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
         }
         if (widget) {
             $state.go('home.widgets-bundles.widget-types.widget-type',
-                {widgetTypeId: widget.id.id});
+                {widgetTypeId: widget.typeId.id});
         } else {
             $mdDialog.show({
                 controller: 'SelectWidgetTypeController',
@@ -177,7 +177,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
 
     function exportWidgetType(event, widget) {
         event.stopPropagation();
-        importExport.exportWidgetType(widget.id.id);
+        importExport.exportWidgetType(widget.typeId.id);
     }
 
     function importWidgetType($event) {
diff --git a/ui/src/scss/constants.scss b/ui/src/scss/constants.scss
index 73697fc..c56d40b 100644
--- a/ui/src/scss/constants.scss
+++ b/ui/src/scss/constants.scss
@@ -20,10 +20,12 @@
 $gray: #eee;
 
 $primary-palette-color: 'indigo';
+$default: '500';
 $hue-1: '300';
 $hue-2: '800';
 $hue-3: 'a100';
 
+$primary-default: #305680; //material-color($primary-palette-color, $default);
 $primary-hue-1: material-color($primary-palette-color, $hue-1);
 $primary-hue-2: material-color($primary-palette-color, $hue-2);
 $primary-hue-3: rgb(207, 216, 220);
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index ab9b0d7..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
@@ -280,8 +319,11 @@ $previewSize: 100px;
   overflow: hidden;
   label {
     width: 100%;
-    font-size: 24px;
+    font-size: 16px;
     text-align: center;
+    @media (min-width: $layout-breakpoint-sm) {
+      font-size: 24px;
+    }
   }
 }
 
@@ -369,6 +411,19 @@ md-tabs.tb-headless {
   }
 }
 
+.md-button.tb-layout-button {
+  width: 100%;
+  height: 100%;
+  max-width: 240px;
+  span {
+    padding: 40px;
+    font-size: 18px;
+    font-weight: 400;
+    white-space: normal;
+    line-height: 18px;
+  }
+}
+
 .md-button.tb-add-new-widget {
   border-style: dashed;
   border-width: 2px;