thingsboard-developers

Merge with master

5/24/2017 4:51:26 AM

Changes

Details

diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 9b00682..d4adebe 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -307,7 +307,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)) {
@@ -380,14 +380,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();
@@ -397,7 +409,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 bebab8b..7cd381c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -24,19 +24,18 @@ import org.thingsboard.server.common.data.Device;
 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.id.UUIDBased;
 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;
-import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.UUID;
+import java.util.stream.Collectors;
 
 @RestController
 @RequestMapping("/api")
@@ -238,4 +237,28 @@ 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);
+        }
+    }
 }
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..92e8655 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
@@ -28,6 +28,7 @@ public class Device extends SearchTextBased<DeviceId> {
     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();
     }
 
@@ -70,6 +72,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 +100,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 +129,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 +151,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/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
index 8c86064..b0ebbfd 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
@@ -26,7 +26,9 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 public interface DashboardService {
     
     public Dashboard findDashboardById(DashboardId dashboardId);
-    
+
+    public DashboardInfo findDashboardInfoById(DashboardId dashboardId);
+
     public Dashboard saveDashboard(Dashboard dashboard);
     
     public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index 8264042..cf554f3 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
@@ -65,6 +65,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
     }
 
     @Override
+    public DashboardInfo findDashboardInfoById(DashboardId dashboardId) {
+        log.trace("Executing findDashboardInfoById [{}]", dashboardId);
+        Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        DashboardInfoEntity dashboardInfoEntity = dashboardInfoDao.findById(dashboardId.getId());
+        return getData(dashboardInfoEntity);
+    }
+
+    @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/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..4715435 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
@@ -53,4 +53,7 @@ public interface DeviceService {
     ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
 
     void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
+
+    ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query);
+
 }
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 4394901..ab6fa4e 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
@@ -16,6 +16,7 @@
 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;
@@ -24,11 +25,14 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
 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;
@@ -37,20 +41,20 @@ import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.model.CustomerEntity;
 import org.thingsboard.server.dao.model.DeviceEntity;
 import org.thingsboard.server.dao.model.TenantEntity;
+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.List;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
-import static org.thingsboard.server.dao.DaoUtil.convertDataList;
-import static org.thingsboard.server.dao.DaoUtil.getData;
-import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
+import static org.thingsboard.server.dao.DaoUtil.*;
 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
@@ -194,6 +198,32 @@ 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;
+    }
+
     private DataValidator<Device> deviceValidator =
             new DataValidator<Device>() {
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
index 740bf2a..69ed92c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
@@ -51,7 +51,10 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
 
     @Column(name = DEVICE_NAME_PROPERTY)
     private String name;
-    
+
+    @Column(name = DEVICE_TYPE_PROPERTY)
+    private String type;
+
     @Column(name = SEARCH_TEXT_PROPERTY)
     private String searchText;
     
@@ -73,6 +76,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();
     }
     
@@ -108,6 +112,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;
     }
@@ -138,6 +150,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;
     }
@@ -171,6 +184,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;
@@ -190,6 +208,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("]");
@@ -207,6 +227,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/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 2efbd5d..0c68c39 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
@@ -120,6 +120,7 @@ public class ModelConstants {
     public static final String DEVICE_TENANT_ID_PROPERTY = TENTANT_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";
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
index efaa987..071697e 100644
--- a/dao/src/main/resources/schema.cql
+++ b/dao/src/main/resources/schema.cql
@@ -156,6 +156,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.device (
 	tenant_id timeuuid,
 	customer_id timeuuid,
 	name text,
+	type text,
 	search_text text,
 	additional_info text,
 	PRIMARY KEY (id, tenant_id, customer_id)
@@ -271,11 +272,11 @@ CREATE TABLE IF NOT EXISTS thingsboard.relation (
 ) WITH CLUSTERING ORDER BY ( relation_type ASC, to_id ASC, to_type ASC);
 
 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 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);
 
 CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle (
     id timeuuid,
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..3f5c55a
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
@@ -0,0 +1,283 @@
+/**
+ * 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.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).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE").get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE").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).get());
+
+        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+
+        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).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).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).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).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).get();
+        Assert.assertEquals(2, relations.size());
+
+        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFrom(parentB).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).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).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).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();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByTo(childB).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/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..d12d025 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -40,7 +40,8 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         saveDeviceAttributes: saveDeviceAttributes,
         deleteDeviceAttributes: deleteDeviceAttributes,
         sendOneWayRpcCommand: sendOneWayRpcCommand,
-        sendTwoWayRpcCommand: sendTwoWayRpcCommand
+        sendTwoWayRpcCommand: sendTwoWayRpcCommand,
+        findByQuery: findByQuery
     }
 
     return service;
@@ -270,4 +271,19 @@ 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;
+    }
+
 }
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index 4904a6a..43c537c 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -20,9 +20,9 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
     .name;
 
 /*@ngInject*/
-function EntityService($http, $q, userService, deviceService,
+function EntityService($http, $q, $filter, $translate, userService, deviceService,
                        assetService, tenantService, customerService,
-                       ruleService, pluginService, types, utils) {
+                       ruleService, pluginService, entityRelationService, attributeService, types, utils) {
     var service = {
         getEntity: getEntity,
         getEntities: getEntities,
@@ -31,7 +31,12 @@ function EntityService($http, $q, userService, deviceService,
         processEntityAliases: processEntityAliases,
         getEntityKeys: getEntityKeys,
         checkEntityAlias: checkEntityAlias,
-        createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
+        createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
+        getRelatedEntities: getRelatedEntities,
+        saveRelatedEntity: saveRelatedEntity,
+        getRelatedEntity: getRelatedEntity,
+        deleteRelatedEntity: deleteRelatedEntity,
+        moveEntity: moveEntity
     };
 
     return service;
@@ -64,14 +69,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;
     }
 
@@ -474,4 +483,295 @@ 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 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/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..fef3273 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);
diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js
index 0bc73d1..57317b5 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,357 @@ 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;
+        } 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..a8d8556 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -100,6 +100,14 @@ export default angular.module('thingsboard.types', [])
                 tenant: "TENANT",
                 customer: "CUSTOMER"
             },
+            entitySearchDirection: {
+                from: "FROM",
+                to: "TO"
+            },
+            entityRelationType: {
+                contains: "Contains",
+                manages: "Manages"
+            },
             eventType: {
                 alarm: {
                     value: "ALARM",
@@ -199,6 +207,9 @@ export default angular.module('thingsboard.types', [])
             systemBundleAlias: {
                 charts: "charts",
                 cards: "cards"
+            },
+            translate: {
+                dashboardStatePrefix: "dashboardState.state."
             }
         }
     ).name;
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index b20c039..f26121c 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;
@@ -199,6 +217,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();
@@ -279,6 +336,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);
         }
@@ -302,20 +360,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;
@@ -326,6 +386,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
 
     loadStDiff();
 
+    function reload() {
+        loadStDiff();
+    }
+
     function loadStDiff() {
         if (vm.getStDiff) {
             var promise = vm.getStDiff();
@@ -568,18 +632,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;
+            }
         }
     }
 
@@ -653,7 +788,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);
@@ -662,7 +797,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();
@@ -678,7 +817,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/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/related-entity-autocomplete.scss b/ui/src/app/components/related-entity-autocomplete.scss
new file mode 100644
index 0000000..32df94f
--- /dev/null
+++ b/ui/src/app/components/related-entity-autocomplete.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-related-entity-autocomplete {
+  .tb-not-found {
+    display: block;
+    line-height: 1.5;
+    height: 48px;
+  }
+  .tb-entity-item {
+    display: block;
+    height: 48px;
+  }
+  li {
+    height: auto !important;
+    white-space: normal !important;
+  }
+}
diff --git a/ui/src/app/components/related-entity-autocomplete.tpl.html b/ui/src/app/components/related-entity-autocomplete.tpl.html
new file mode 100644
index 0000000..a5b50e9
--- /dev/null
+++ b/ui/src/app/components/related-entity-autocomplete.tpl.html
@@ -0,0 +1,43 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-autocomplete ng-required="tbRequired"
+                 ng-disabled="disabled"
+                 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="{{ placeholderText | translate }}"
+                 md-menu-class="tb-related-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>{{ notFoundText | translate:{entity: entitySearchText} }}</span>
+        </div>
+    </md-not-found>
+    <div ng-messages="theForm.entity.$error">
+        <div ng-message="required">{{ requiredText | translate }}</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index ec5e7ed..6fc4bba 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..122889d 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();
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..e18ebb8 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,33 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     Object.defineProperty(vm, 'toolbarOpened', {
-        get: function() { return vm.isToolbarOpened || vm.isEdit; },
+        get: function() { return !vm.widgetEditMode && ($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 +102,78 @@ export default function DashboardController(types, dashboardUtils, widgetService
         });
     }
 
+    vm.showCloseToolbar = function() {
+        return !$scope.forceFullscreen && !vm.isEdit && !vm.showRightLayoutSwitch();
+    }
+
+    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 +182,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 +251,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
         }
     });
 
+    loadDashboard();
 
     function loadWidgetLibrary() {
         vm.latestWidgetTypes = [];
@@ -199,34 +319,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 +351,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 +427,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 +443,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 +460,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 +578,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 +628,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 +669,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 +680,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 +705,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() {
@@ -549,36 +739,45 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     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 +787,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 +830,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 +876,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 +903,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 +917,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,37 +940,66 @@ 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;
+                    openDashboardState(vm.prevDashboardState);
                     entityAliasesUpdated();
                 }
             }
         }
     }
 
+    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();
+                }
+            }
+        }
+    }
+
     function toggleDashboardEditMode() {
         setEditMode(!vm.isEdit, true);
     }
@@ -756,20 +1024,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..bc5ec56 100644
--- a/ui/src/app/dashboard/dashboard.scss
+++ b/ui/src/app/dashboard/dashboard.scss
@@ -63,7 +63,7 @@ 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 {
@@ -118,6 +118,27 @@ section.tb-dashboard-toolbar {
           .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;
+                }
+              }
+            }
+          }
         }
       }
     }
@@ -133,6 +154,19 @@ section.tb-dashboard-toolbar {
      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..078daab 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -16,16 +16,10 @@
 
 -->
 <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-fab-toolbar ng-show="!vm.widgetEditMode" 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()">
@@ -37,77 +31,100 @@
             </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()">
-                        <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 id="dashboard-expand-button"
-                               aria-label="{{ 'fullscreen.fullscreen' | translate }}"
-                               class="md-icon-button">
-                    </md-button>
-                    <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
-                    </tb-user-menu>
-                    <md-button ng-show="vm.isEdit || vm.displayExport()"
-                               aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
-                               ng-click="vm.exportDashboard($event)">
-                        <md-tooltip md-direction="bottom">
-                            {{ 'dashboard.export' | translate }}
-                        </md-tooltip>
-                        <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
-                    </md-button>
-                    <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
-                                   is-toolbar
-                                   direction="left"
-                                   tooltip-direction="bottom" aggregation
-                                   ng-model="vm.dashboardConfiguration.timewindow">
-                    </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">
-                    </tb-aliases-entity-select>
-                    <md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
-                               ng-click="vm.openEntityAliases($event)">
-                        <md-tooltip md-direction="bottom">
-                            {{ 'entity.aliases' | translate }}
-                        </md-tooltip>
-                        <md-icon aria-label="{{ 'entity.aliases' | translate }}" class="material-icons">devices_other</md-icon>
-                    </md-button>
-                    <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
-                               ng-click="vm.openDashboardSettings($event)">
-                        <md-tooltip md-direction="bottom">
-                            {{ 'dashboard.settings' | translate }}
-                        </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"
-                                           ng-model="vm.currentDashboardId"
-                                           dashboards-scope="{{vm.currentDashboardScope}}"
-                                           customer-id="vm.currentCustomerId">
-                    </tb-dashboard-select>
+                    <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">
+                            </md-button>
+                            <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
+                            </tb-user-menu>
+                            <md-button ng-show="vm.isEdit || vm.displayExport()"
+                                       aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
+                                       ng-click="vm.exportDashboard($event)">
+                                <md-tooltip md-direction="bottom">
+                                    {{ 'dashboard.export' | translate }}
+                                </md-tooltip>
+                                <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
+                            </md-button>
+                            <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
+                                           is-toolbar
+                                           direction="left"
+                                           tooltip-direction="bottom" aggregation
+                                           ng-model="vm.dashboardConfiguration.timewindow">
+                            </tb-timewindow>
+                            <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
+                                                      tooltip-direction="bottom"
+                                                      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)">
+                                <md-tooltip md-direction="bottom">
+                                    {{ 'entity.aliases' | translate }}
+                                </md-tooltip>
+                                <md-icon aria-label="{{ 'entity.aliases' | translate }}" class="material-icons">devices_other</md-icon>
+                            </md-button>
+                            <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
+                                       ng-click="vm.openDashboardSettings($event)">
+                                <md-tooltip md-direction="bottom">
+                                    {{ 'dashboard.settings' | translate }}
+                                </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 && vm.displayDashboardsSelect()"
+                                                   ng-model="vm.currentDashboardId"
+                                                   dashboards-scope="{{vm.currentDashboardScope}}"
+                                                   customer-id="vm.currentCustomerId">
+                            </tb-dashboard-select>
+                        </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>
                 </md-fab-actions>
             </md-toolbar>
         </md-fab-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>
         <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 +133,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 +191,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 +305,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 +315,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 +327,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..dbe4cbe 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,49 @@ 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;
+        }
 
-    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.showEntitiesSelect)) {
+            vm.settings.showEntitiesSelect = true;
+        }
 
-    vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%';
+        if (angular.isUndefined(vm.settings.showDashboardTimewindow)) {
+            vm.settings.showDashboardTimewindow = true;
+        }
+
+        if (angular.isUndefined(vm.settings.showDashboardExport)) {
+            vm.settings.showDashboardExport = true;
+        }
+    }
+
+    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 +93,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..88fc66d 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,53 @@
         <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.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 +85,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/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..73a97a1 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';
@@ -46,7 +48,6 @@ import EditWidgetDirective from './edit-widget.directive';
 
 export default angular.module('thingsboard.dashboard', [
     uiRouter,
-    gridster.name,
     thingsboardTypes,
     thingsboardItemBuffer,
     thingsboardImportExport,
@@ -58,10 +59,13 @@ export default angular.module('thingsboard.dashboard', [
     thingsboardDetailsSidenav,
     thingsboardWidgetConfig,
     thingsboardDashboardSelect,
+    thingsboardRelatedEntityAutocomplete,
     thingsboardDashboard,
     thingsboardExpandFullscreen,
     thingsboardWidgetsBundleSelect,
-    thingsboardSocialsharePanel
+    thingsboardSocialsharePanel,
+    dashboardLayouts,
+    dashboardStates
 ])
     .config(DashboardRoutes)
     .controller('DashboardsController', DashboardsController)
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.controller.js b/ui/src/app/dashboard/layouts/select-target-layout.controller.js
new file mode 100644
index 0000000..1af289a
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/select-target-layout.controller.js
@@ -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.
+ */
+
+/*@ngInject*/
+export default function SelectTargetLayoutController($scope, $mdDialog) {
+
+    var vm = this;
+
+    vm.cancel = cancel;
+    vm.selectLayout = selectLayout;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function selectLayout($event, layoutId) {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(layoutId);
+    }
+}
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/dashboard-state-dialog.tpl.html b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
new file mode 100644
index 0000000..fa45d7a
--- /dev/null
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+    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 class="dashboard-state" aria-label="{{'dashboard.state' | translate }}" style="min-width: 600px;">
+    <form name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2>{{ (vm.isAdd ? 'dashboard.add-state' : 'dashboard.edit-state') | translate }}</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'action.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <fieldset ng-disabled="loading">
+                        <md-input-container class="md-block">
+                            <label translate>dashboard.state-name</label>
+                            <input name="name" required ng-model="vm.state.name">
+                            <div ng-messages="theForm.name.$error">
+                                <div ng-message="required" translate>dashboard.state-name-required</div>
+                                <div ng-message="stateExists" translate>dashboard.state-name-exists</div>
+                            </div>
+                        </md-input-container>
+                        <md-input-container class="md-block">
+                            <label translate>dashboard.state-id</label>
+                            <input name="stateId" ng-model="vm.state.id"
+                                   ng-change="vm.stateIdTouched = true"
+                                   ng-pattern="/^[a-zA-Z0-9_]*$/">
+                            <div ng-messages="theForm.stateId.$error">
+                                <div ng-message="required" translate>dashboard.state-id-required</div>
+                                <div ng-message="stateExists" translate>dashboard.state-id-exists</div>
+                                <div ng-message="pattern" translate>dashboard.invalid-state-id-format</div>
+                            </div>
+                        </md-input-container>
+                        <md-checkbox flex aria-label="{{ 'dashboard.is-root-state' | translate }}"
+                                     ng-model="vm.state.root">{{ 'dashboard.is-root-state' | translate }}
+                        </md-checkbox>
+                    </fieldset>
+                </md-content>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ vm.isAdd ? 'Add' : 'Save' }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
+                Cancel
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
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/default-state-controller.tpl.html b/ui/src/app/dashboard/states/default-state-controller.tpl.html
new file mode 100644
index 0000000..9de1563
--- /dev/null
+++ b/ui/src/app/dashboard/states/default-state-controller.tpl.html
@@ -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.
+
+-->
+<md-select ng-show="vm.displayStateSelection()" aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.stateObject[0].id">
+    <md-option ng-repeat="(stateId, state) in vm.states" ng-value="stateId">
+        {{vm.getStateName(stateId, state)}}
+    </md-option>
+</md-select>
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..3eaf453
--- /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 = entityService.entityName(params.entityId.entityType, entity);
+                    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.scss b/ui/src/app/dashboard/states/manage-dashboard-states.scss
new file mode 100644
index 0000000..53e6724
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.scss
@@ -0,0 +1,34 @@
+/**
+ * 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.
+ */
+
+.manage-dashboard-states {
+  table.md-table {
+    tbody {
+      tr {
+        td {
+          &.tb-action-cell {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            min-width: 100px;
+            max-width: 100px;
+            width: 100px;
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
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..151c05a
--- /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 md-theme="tb-search-input" 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/select-target-state.tpl.html b/ui/src/app/dashboard/states/select-target-state.tpl.html
new file mode 100644
index 0000000..c874a07
--- /dev/null
+++ b/ui/src/app/dashboard/states/select-target-state.tpl.html
@@ -0,0 +1,50 @@
+<!--
+
+    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.select-state' | translate }}">
+    <form name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>{{ 'dashboard.select-state' }}</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">
+                    <md-select required aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.stateId">
+                        <md-option ng-repeat="(stateId, state) in vm.states" ng-value="stateId">
+                            {{state.name}}
+                        </md-option>
+                    </md-select>
+                </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/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/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/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/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 1fc45f7..7f52eac 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}}"
@@ -324,6 +329,7 @@ export default angular.module('thingsboard.locale', [])
                     "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
                     "display-title": "Display dashboard title",
                     "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,7 +356,29 @@ 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",
@@ -569,6 +597,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",
diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js
new file mode 100644
index 0000000..d227041
--- /dev/null
+++ b/ui/src/app/locale/translate-handler.js
@@ -0,0 +1,26 @@
+/*
+ * 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 ThingsboardMissingTranslateHandler($log, types) {
+
+    return function (translationId) {
+        if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix)) {
+            $log.warn('Translation for ' + translationId + ' doesn\'t exist');
+        }
+    };
+
+}
\ No newline at end of file
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/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/main.scss b/ui/src/scss/main.scss
index ab9b0d7..4ffabc9 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -280,8 +280,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 +372,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;