thingsboard-aplcache

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 5647b52..b66cd89 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -157,6 +157,16 @@ public abstract class BaseController {
         }
     }
 
+    void checkArrayParameter(String name, String[] params) throws ThingsboardException {
+        if (params == null || params.length == 0) {
+            throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+        } else {
+            for (String param : params) {
+                checkParameter(name, param);
+            }
+        }
+    }
+
     UUID toUUID(String id) {
         return UUID.fromString(id);
     }
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 5743321..b08a964 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.controller;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -22,6 +23,7 @@ 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;
@@ -29,6 +31,11 @@ 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;
 
 @RestController
 @RequestMapping("/api")
@@ -189,4 +196,30 @@ public class DeviceController extends BaseController {
             throw handleException(e);
         }
     }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
+    @ResponseBody
+    public List<Device> getDevicesByIds(
+            @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException {
+        checkArrayParameter("deviceIds", strDeviceIds);
+        try {
+            SecurityUser user = getCurrentUser();
+            TenantId tenantId = user.getTenantId();
+            CustomerId customerId = user.getCustomerId();
+            List<DeviceId> deviceIds = new ArrayList<>();
+            for (String strDeviceId : strDeviceIds) {
+                deviceIds.add(new DeviceId(toUUID(strDeviceId)));
+            }
+            ListenableFuture<List<Device>> devices;
+            if (customerId == null || customerId.isNullUid()) {
+                devices = deviceService.findDevicesByTenantIdAndIdsAsync(tenantId, deviceIds);
+            } else {
+                devices = deviceService.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, deviceIds);
+            }
+            return checkNotNull(devices.get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
index 60c833b..01346b0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
@@ -64,6 +64,27 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
         return list;
     }
 
+    protected ListenableFuture<List<T>> findListByStatementAsync(Statement statement) {
+        if (statement != null) {
+            statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
+            ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+            ListenableFuture<List<T>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<T>>() {
+                @Nullable
+                @Override
+                public List<T> apply(@Nullable ResultSet resultSet) {
+                    Result<T> result = getMapper().map(resultSet);
+                    if (result != null) {
+                        return result.all();
+                    } else {
+                        return Collections.emptyList();
+                    }
+                }
+            });
+            return result;
+        }
+        return Futures.immediateFuture(Collections.emptyList());
+    }
+
     protected T findOneByStatement(Statement statement) {
         T object = null;
         if (statement != null) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
index fa98b86..27499bb 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
@@ -56,4 +56,12 @@ public abstract class DaoUtil {
         return id;
     }
 
+    public static List<UUID> toUUIDs(List<? extends UUIDBased> idBasedIds) {
+        List<UUID> ids = new ArrayList<>();
+        for (UUIDBased idBased : idBasedIds) {
+            ids.add(getId(idBased));
+        }
+        return ids;
+    }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
index 5803416..b8d395c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
@@ -19,6 +19,7 @@ import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.Dao;
@@ -46,7 +47,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
      * @return the list of device objects
      */
     List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink);
-    
+
+    /**
+     * Find devices by tenantId and devices Ids.
+     *
+     * @param tenantId the tenantId
+     * @param deviceIds the device Ids
+     * @return the list of device objects
+     */
+    ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds);
+
     /**
      * Find devices by tenantId, customerId and page link.
      *
@@ -58,6 +68,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
     List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
 
     /**
+     * Find devices by tenantId, customerId and devices Ids.
+     *
+     * @param tenantId the tenantId
+     * @param customerId the customerId
+     * @param deviceIds the device Ids
+     * @return the list of device objects
+     */
+    ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds);
+
+    /**
      * Find devices by tenantId and device name.
      *
      * @param tenantId the tenantId
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
index 540204d..81fc0bc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
@@ -16,12 +16,14 @@
 package org.thingsboard.server.dao.device;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
 import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 import java.util.*;
 
 import com.datastax.driver.core.querybuilder.Select;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Device;
@@ -62,6 +64,16 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
     }
 
     @Override
+    public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
+        log.debug("Try to find devices by tenantId [{}] and device Ids [{}]", tenantId, deviceIds);
+        Select select = select().from(getColumnFamilyName());
+        Select.Where query = select.where();
+        query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
+        query.and(in(ID_PROPERTY, deviceIds));
+        return findListByStatementAsync(query);
+    }
+
+    @Override
     public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
         log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
         List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
@@ -74,6 +86,17 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
     }
 
     @Override
+    public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
+        log.debug("Try to find devices by tenantId [{}], customerId [{}] and device Ids [{}]", tenantId, customerId, deviceIds);
+        Select select = select().from(getColumnFamilyName());
+        Select.Where query = select.where();
+        query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
+        query.and(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId));
+        query.and(in(ID_PROPERTY, deviceIds));
+        return findListByStatementAsync(query);
+    }
+
+    @Override
     public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) {
         Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME);
         Select.Where query = select.where();
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 b760f55..35d3496 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
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 
+import java.util.List;
 import java.util.Optional;
 
 public interface DeviceService {
@@ -43,9 +44,13 @@ public interface DeviceService {
 
     TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
 
+    ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
+
     void deleteDevicesByTenantId(TenantId tenantId);
 
     TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
 
+    ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
+
     void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
 }
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 214d37c..3d1ce31 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
@@ -45,8 +45,10 @@ import java.util.Optional;
 
 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.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;
 
 @Service
@@ -144,6 +146,16 @@ public class DeviceServiceImpl implements DeviceService {
     }
 
     @Override
+    public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
+        log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
+        ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceIds));
+        return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
+    }
+
+
+    @Override
     public void deleteDevicesByTenantId(TenantId tenantId) {
         log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
         validateId(tenantId, "Incorrect tenantId " + tenantId);
@@ -162,6 +174,17 @@ public class DeviceServiceImpl implements DeviceService {
     }
 
     @Override
+    public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
+        log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
+        ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(),
+                customerId.getId(), toUUIDs(deviceIds));
+        return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
+    }
+
+    @Override
     public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) {
         log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId);
         validateId(tenantId, "Incorrect tenantId " + tenantId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
index 1976eb9..70e9860 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
@@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 
+import java.util.List;
 import java.util.UUID;
 
 public class Validator {
@@ -78,6 +79,23 @@ public class Validator {
     }
 
     /**
+     * This method validate list of <code>UUIDBased</code> ids. If at least one of the ids is null than throw
+     * <code>IncorrectParameterException</code> exception
+     *
+     * @param ids          the list of ids
+     * @param errorMessage the error message for exception
+     */
+    public static void validateIds(List<? extends UUIDBased> ids, String errorMessage) {
+        if (ids == null || ids.isEmpty()) {
+            throw new IncorrectParameterException(errorMessage);
+        } else {
+            for (UUIDBased id : ids) {
+                validateId(id, errorMessage);
+            }
+        }
+    }
+
+    /**
      * This method validate <code>PageLink</code> page link. If pageLink is invalid than throw
      * <code>IncorrectParameterException</code> exception
      *
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index 118afb9..56079af 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -188,7 +188,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges',
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
 VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table',
-'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<md-tabs md-selected=\"sourceIndex\" ng-class=\"{''tb-headless'': sources.length === 1}\"\n    id=\"tabs\" md-border-bottom flex class=\"tb-absolute-fill\">\n    <md-tab ng-repeat=\"source in sources\" label=\"{{ source.label }}\">\n        <md-table-container>\n            <table md-table>\n                <thead md-head md-order=\"source.query.order\" md-on-reorder=\"onReorder(source)\">\n                    <tr md-row>\n                        <th ng-show=\"showTimestamp\" md-column md-order-by=\"0\"><span>Timestamp</span></th>\n                        <th md-column md-order-by=\"{{ h.index }}\" ng-repeat=\"h in source.ts.header\"><span>{{ h.label }}</span></th>\n                    </tr>\n                </thead>\n                <tbody md-body>\n                    <tr md-row ng-repeat=\"row in source.ts.data\">\n                        <td ng-show=\"$index > 0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n                        </td>\n                    </tr>    \n                </tbody>    \n            </table>\n        </md-table-container>\n        <md-table-pagination md-limit=\"source.query.limit\" md-limit-options=\"[5, 10, 15]\"\n                             md-page=\"source.query.page\" md-total=\"{{source.ts.count}}\"\n                             md-on-paginate=\"onPaginate(source)\" md-page-select>\n        </md-table-pagination>\n    </md-tab>\n</md-tabs>","templateCss":"table.md-table thead.md-head>tr.md-row {\n    height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n    height: 38px;\n}\n\n.md-table-pagination>* {\n    height: 46px;\n}\n","controllerScript":"self.onInit = function() {\n    \n    var scope = self.ctx.$scope;\n    \n    self.ctx.filter = scope.$injector.get(\"$filter\");\n\n    scope.sources = [];\n    scope.sourceIndex = 0;\n    scope.showTimestamp = self.ctx.settings.showTimestamp !== false;\n    \n    var keyOffset = 0;\n    for (var ds in self.ctx.datasources) {\n        var source = {};\n        var datasource = self.ctx.datasources[ds];\n        source.keyStartIndex = keyOffset;\n        keyOffset += datasource.dataKeys.length;\n        source.keyEndIndex = keyOffset;\n        source.label = datasource.name;\n        source.data = [];\n        source.rawData = [];\n        source.query = {\n            limit: 5,\n            page: 1,\n            order: ''-0''\n        }\n        source.ts = {\n            header: [],\n            count: 0,\n            data: [],\n            stylesInfo: [],\n            contentsInfo: [],\n            rowDataTemplate: {}\n        }\n        source.ts.rowDataTemplate[''Timestamp''] = null;\n        for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n            var dataKey = datasource.dataKeys[a];\n            var keySettings = dataKey.settings;\n            source.ts.header.push({\n                index: a+1,\n                label: dataKey.label\n            });\n            source.ts.rowDataTemplate[dataKey.label] = null;\n\n            var cellStyleFunction = null;\n            var useCellStyleFunction = false;\n            \n            if (keySettings.useCellStyleFunction === true) {\n                if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n                    try {\n                       cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n                       useCellStyleFunction = true;\n                    } catch (e) {\n                       cellStyleFunction = null;\n                       useCellStyleFunction = false;\n                    }\n                }\n            }\n\n            source.ts.stylesInfo.push({\n                useCellStyleFunction: useCellStyleFunction,\n                cellStyleFunction: cellStyleFunction\n            });\n            \n            var cellContentFunction = null;\n            var useCellContentFunction = false;\n            \n            if (keySettings.useCellContentFunction === true) {\n                if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n                    try {\n                       cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n                       useCellContentFunction = true;\n                    } catch (e) {\n                       cellContentFunction = null;\n                       useCellContentFunction = false;\n                    }\n                }\n            }\n            \n            source.ts.contentsInfo.push({\n                useCellContentFunction: useCellContentFunction,\n                cellContentFunction: cellContentFunction\n            });\n            \n        }\n        scope.sources.push(source);\n    }\n\n    scope.onPaginate = function(source) {\n        updatePage(source);\n    }\n    \n    scope.onReorder = function(source) {\n        reorder(source);\n        updatePage(source);\n    }\n    \n    scope.cellStyle = function(source, index, value) {\n        var style = {};\n        if (index > 0) {\n            var styleInfo = source.ts.stylesInfo[index-1];\n            if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n                try {\n                    style = styleInfo.cellStyleFunction(value);\n                } catch (e) {\n                    style = {};\n                }\n            }\n        }\n        return style;\n    }\n\n    scope.cellContent = function(source, index, row, value) {\n        if (index === 0) {\n            return self.ctx.filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n        } else {\n            var strContent = '''';\n            if (angular.isDefined(value)) {\n                strContent = ''''+value;\n            }\n            var content = strContent;\n            var contentInfo = source.ts.contentsInfo[index-1];\n            if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n                try {\n                    var rowData = source.ts.rowDataTemplate;\n                    rowData[''Timestamp''] = row[0];\n                    for (var h in source.ts.header) {\n                        var headerInfo = source.ts.header[h];\n                        rowData[headerInfo.label] = row[headerInfo.index];\n                    }\n                    content = contentInfo.cellContentFunction(value, rowData, filter);\n                } catch (e) {\n                    content = strContent;\n                }\n            }            \n            return content;\n        }\n    }\n    \n    scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n       if (newIndex != oldIndex) {\n           updateSourceData(scope.sources[scope.sourceIndex]);\n       } \n    });\n}\n\nself.onDataUpdated = function() {\n    var scope = self.ctx.$scope;\n    for (var s in scope.sources) {\n        var source = scope.sources[s];\n        source.rawData = self.ctx.data.slice(source.keyStartIndex, source.keyEndIndex);\n    }\n    updateSourceData(scope.sources[scope.sourceIndex]);\n    scope.$digest();\n}\n\nself.onDestroy = function() {\n}\n\nfunction updatePage(source) {\n    var startIndex = source.query.limit * (source.query.page - 1);\n    source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n    source.data = self.ctx.filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n    var rowsMap = [];\n    for (var d = 0; d < data.length; d++) {\n        var columnData = data[d].data;\n        for (var i = 0; i < columnData.length; i++) {\n            var cellData = columnData[i];\n            var timestamp = cellData[0];\n            var row = rowsMap[timestamp];\n            if (!row) {\n                row = [];\n                row[0] = timestamp;\n                for (var c = 0; c < data.length; c++) {\n                    row[c+1] = undefined;\n                }\n                rowsMap[timestamp] = row;\n            }\n            row[d+1] = cellData[1];\n        }\n    }\n    var rows = [];\n    for (var t in rowsMap) {\n        rows.push(rowsMap[t]);\n    }\n    return rows;\n}\n\nfunction updateSourceData(source) {\n    source.data = convertData(source.rawData);\n    source.ts.count = source.data.length;\n    reorder(source);\n    updatePage(source);\n}\n","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"TimeseriesTableSettings\",\n        \"properties\": {\n            \"showTimestamp\": {\n                \"title\": \"Display timestamp column\",\n                \"type\": \"boolean\",\n                \"default\": true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"showTimestamp\"\n    ]\n}","dataKeySettingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"useCellContentFunction\": {\n                \"title\": \"Use cell content function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellContentFunction\": {\n                \"title\": \"Cell content function: f(value, rowData, filter)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        },\n        \"useCellContentFunction\",\n        {\n            \"key\": \"cellContentFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature  °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = (value + 60)/120 * 100;\\n    var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n    color.setAlpha(.5);\\n    return {\\n      paddingLeft: ''20px'',\\n      color: ''#ffffff'',\\n      background: color.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = value;\\n    var backgroundColor = tinycolor(''blue'');\\n    backgroundColor.setAlpha(value/100);\\n    var color = ''blue'';\\n    if (value > 50) {\\n        color = ''white'';\\n    }\\n    \\n    return {\\n      paddingLeft: ''20px'',\\n      color: color,\\n      background: backgroundColor.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}',
+'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<md-tabs md-selected=\"sourceIndex\" ng-class=\"{''tb-headless'': sources.length === 1}\"\n    id=\"tabs\" md-border-bottom flex class=\"tb-absolute-fill\">\n    <md-tab ng-repeat=\"source in sources\" label=\"{{ source.datasource.name }}\">\n        <md-table-container>\n            <table md-table>\n                <thead md-head md-order=\"source.query.order\" md-on-reorder=\"onReorder(source)\">\n                    <tr md-row>\n                        <th ng-show=\"showTimestamp\" md-column md-order-by=\"0\"><span>Timestamp</span></th>\n                        <th md-column md-order-by=\"{{ h.index }}\" ng-repeat=\"h in source.ts.header\"><span>{{ h.dataKey.label }}</span></th>\n                    </tr>\n                </thead>\n                <tbody md-body>\n                    <tr md-row ng-repeat=\"row in source.ts.data\">\n                        <td ng-show=\"$index > 0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n                        </td>\n                    </tr>    \n                </tbody>    \n            </table>\n        </md-table-container>\n        <md-table-pagination md-limit=\"source.query.limit\" md-limit-options=\"[5, 10, 15]\"\n                             md-page=\"source.query.page\" md-total=\"{{source.ts.count}}\"\n                             md-on-paginate=\"onPaginate(source)\" md-page-select>\n        </md-table-pagination>\n    </md-tab>\n</md-tabs>","templateCss":"table.md-table thead.md-head>tr.md-row {\n    height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n    height: 38px;\n}\n\n.md-table-pagination>* {\n    height: 46px;\n}\n","controllerScript":"self.onInit = function() {\n    \n    var scope = self.ctx.$scope;\n    \n    self.ctx.filter = scope.$injector.get(\"$filter\");\n\n    scope.sources = [];\n    scope.sourceIndex = 0;\n    scope.showTimestamp = self.ctx.settings.showTimestamp !== false;\n    \n    var keyOffset = 0;\n    for (var ds in self.ctx.datasources) {\n        var source = {};\n        var datasource = self.ctx.datasources[ds];\n        source.keyStartIndex = keyOffset;\n        keyOffset += datasource.dataKeys.length;\n        source.keyEndIndex = keyOffset;\n        source.datasource = datasource;\n        source.data = [];\n        source.rawData = [];\n        source.query = {\n            limit: 5,\n            page: 1,\n            order: ''-0''\n        }\n        source.ts = {\n            header: [],\n            count: 0,\n            data: [],\n            stylesInfo: [],\n            contentsInfo: [],\n            rowDataTemplate: {}\n        }\n        source.ts.rowDataTemplate[''Timestamp''] = null;\n        for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n            var dataKey = datasource.dataKeys[a];\n            var keySettings = dataKey.settings;\n            source.ts.header.push({\n                index: a+1,\n                dataKey: dataKey\n            });\n            source.ts.rowDataTemplate[dataKey.label] = null;\n\n            var cellStyleFunction = null;\n            var useCellStyleFunction = false;\n            \n            if (keySettings.useCellStyleFunction === true) {\n                if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n                    try {\n                       cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n                       useCellStyleFunction = true;\n                    } catch (e) {\n                       cellStyleFunction = null;\n                       useCellStyleFunction = false;\n                    }\n                }\n            }\n\n            source.ts.stylesInfo.push({\n                useCellStyleFunction: useCellStyleFunction,\n                cellStyleFunction: cellStyleFunction\n            });\n            \n            var cellContentFunction = null;\n            var useCellContentFunction = false;\n            \n            if (keySettings.useCellContentFunction === true) {\n                if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n                    try {\n                       cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n                       useCellContentFunction = true;\n                    } catch (e) {\n                       cellContentFunction = null;\n                       useCellContentFunction = false;\n                    }\n                }\n            }\n            \n            source.ts.contentsInfo.push({\n                useCellContentFunction: useCellContentFunction,\n                cellContentFunction: cellContentFunction\n            });\n            \n        }\n        scope.sources.push(source);\n    }\n\n    scope.onPaginate = function(source) {\n        updatePage(source);\n    }\n    \n    scope.onReorder = function(source) {\n        reorder(source);\n        updatePage(source);\n    }\n    \n    scope.cellStyle = function(source, index, value) {\n        var style = {};\n        if (index > 0) {\n            var styleInfo = source.ts.stylesInfo[index-1];\n            if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n                try {\n                    style = styleInfo.cellStyleFunction(value);\n                } catch (e) {\n                    style = {};\n                }\n            }\n        }\n        return style;\n    }\n\n    scope.cellContent = function(source, index, row, value) {\n        if (index === 0) {\n            return self.ctx.filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n        } else {\n            var strContent = '''';\n            if (angular.isDefined(value)) {\n                strContent = ''''+value;\n            }\n            var content = strContent;\n            var contentInfo = source.ts.contentsInfo[index-1];\n            if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n                try {\n                    var rowData = source.ts.rowDataTemplate;\n                    rowData[''Timestamp''] = row[0];\n                    for (var h in source.ts.header) {\n                        var headerInfo = source.ts.header[h];\n                        rowData[headerInfo.dataKey.name] = row[headerInfo.index];\n                    }\n                    content = contentInfo.cellContentFunction(value, rowData, self.ctx.filter);\n                } catch (e) {\n                    content = strContent;\n                }\n            }            \n            return content;\n        }\n    }\n    \n    scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n       if (newIndex != oldIndex) {\n           updateSourceData(scope.sources[scope.sourceIndex]);\n       } \n    });\n}\n\nself.onDataUpdated = function() {\n    var scope = self.ctx.$scope;\n    for (var s in scope.sources) {\n        var source = scope.sources[s];\n        source.rawData = self.ctx.data.slice(source.keyStartIndex, source.keyEndIndex);\n    }\n    updateSourceData(scope.sources[scope.sourceIndex]);\n    scope.$digest();\n}\n\nself.onDestroy = function() {\n}\n\nfunction updatePage(source) {\n    var startIndex = source.query.limit * (source.query.page - 1);\n    source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n    source.data = self.ctx.filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n    var rowsMap = [];\n    for (var d = 0; d < data.length; d++) {\n        var columnData = data[d].data;\n        for (var i = 0; i < columnData.length; i++) {\n            var cellData = columnData[i];\n            var timestamp = cellData[0];\n            var row = rowsMap[timestamp];\n            if (!row) {\n                row = [];\n                row[0] = timestamp;\n                for (var c = 0; c < data.length; c++) {\n                    row[c+1] = undefined;\n                }\n                rowsMap[timestamp] = row;\n            }\n            row[d+1] = cellData[1];\n        }\n    }\n    var rows = [];\n    for (var t in rowsMap) {\n        rows.push(rowsMap[t]);\n    }\n    return rows;\n}\n\nfunction updateSourceData(source) {\n    source.data = convertData(source.rawData);\n    source.ts.count = source.data.length;\n    reorder(source);\n    updatePage(source);\n}\n","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"TimeseriesTableSettings\",\n        \"properties\": {\n            \"showTimestamp\": {\n                \"title\": \"Display timestamp column\",\n                \"type\": \"boolean\",\n                \"default\": true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"showTimestamp\"\n    ]\n}","dataKeySettingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"useCellContentFunction\": {\n                \"title\": \"Use cell content function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellContentFunction\": {\n                \"title\": \"Cell content function: f(value, rowData, filter)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        },\n        \"useCellContentFunction\",\n        {\n            \"key\": \"cellContentFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature  °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = (value + 60)/120 * 100;\\n    var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n    color.setAlpha(.5);\\n    return {\\n      paddingLeft: ''20px'',\\n      color: ''#ffffff'',\\n      background: color.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = value;\\n    var backgroundColor = tinycolor(''blue'');\\n    backgroundColor.setAlpha(value/100);\\n    var color = ''blue'';\\n    if (value > 50) {\\n        color = ''white'';\\n    }\\n    \\n    return {\\n      paddingLeft: ''20px'',\\n      color: color,\\n      background: backgroundColor.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}',
 'Timeseries table' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index dfa3d97..60ccad3 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
     .name;
 
 /*@ngInject*/
-function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
+function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
 
 
     var deviceAttributesSubscriptionMap = {};
@@ -30,6 +30,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
         deleteDevice: deleteDevice,
         getCustomerDevices: getCustomerDevices,
         getDevice: getDevice,
+        getDevices: getDevices,
+        processDeviceAliases: processDeviceAliases,
+        checkDeviceAlias: checkDeviceAlias,
         getDeviceCredentials: getDeviceCredentials,
         getDeviceKeys: getDeviceKeys,
         getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@@ -99,6 +102,200 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
         return deferred.promise;
     }
 
+    function getDevices(deviceIds) {
+        var deferred = $q.defer();
+        var ids = '';
+        for (var i=0;i<deviceIds.length;i++) {
+            if (i>0) {
+                ids += ',';
+            }
+            ids += deviceIds[i];
+        }
+        var url = '/api/devices?deviceIds=' + ids;
+        $http.get(url, null).then(function success(response) {
+            var devices = response.data;
+            devices.sort(function (device1, device2) {
+               var id1 =  device1.id.id;
+               var id2 =  device2.id.id;
+               var index1 = deviceIds.indexOf(id1);
+               var index2 = deviceIds.indexOf(id2);
+               return index1 - index2;
+            });
+            deferred.resolve(devices);
+        }, function fail(response) {
+            deferred.reject(response.data);
+        });
+        return deferred.promise;
+    }
+
+    function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
+        var deferred = $q.defer();
+        var user = userService.getCurrentUser();
+        var promise;
+        var pageLink = {limit: limit, textSearch: deviceNameFilter};
+        if (user.authority === 'CUSTOMER_USER') {
+            var customerId = user.customerId;
+            promise = getCustomerDevices(customerId, pageLink);
+        } else {
+            promise = getTenantDevices(pageLink);
+        }
+        promise.then(
+            function success(result) {
+                if (result.data && result.data.length > 0) {
+                    deferred.resolve(result.data);
+                } else {
+                    deferred.resolve(null);
+                }
+            },
+            function fail() {
+                deferred.resolve(null);
+            }
+        );
+        return deferred.promise;
+    }
+
+    function deviceToDeviceInfo(device) {
+        return { name: device.name, id: device.id.id };
+    }
+
+    function devicesToDevicesInfo(devices) {
+        var devicesInfo = [];
+        for (var d in devices) {
+            devicesInfo.push(deviceToDeviceInfo(devices[d]));
+        }
+        return devicesInfo;
+    }
+
+    function processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred) {
+        if (index < aliasIds.length) {
+            var aliasId = aliasIds[index];
+            var deviceAlias = deviceAliases[aliasId];
+            var alias = deviceAlias.alias;
+            if (!deviceAlias.deviceFilter) {
+                getDevice(deviceAlias.deviceId).then(
+                    function success(device) {
+                        var resolvedAlias = {alias: alias, deviceId: device.id.id};
+                        resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
+                        resolution.aliasesInfo.deviceAliasesInfo[aliasId] = [
+                            deviceToDeviceInfo(device)
+                        ];
+                        index++;
+                        processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
+                    },
+                    function fail() {
+                        if (!resolution.error) {
+                            resolution.error = 'dashboard.invalid-aliases-config';
+                        }
+                        index++;
+                        processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
+                    }
+                );
+            } else {
+                var deviceFilter = deviceAlias.deviceFilter;
+                if (deviceFilter.useFilter) {
+                    var deviceNameFilter = deviceFilter.deviceNameFilter;
+                    fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
+                        function(devices) {
+                            if (devices && devices != null) {
+                                var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
+                                resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
+                                resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
+                                index++;
+                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
+                            } else {
+                                if (!resolution.error) {
+                                    resolution.error = 'dashboard.invalid-aliases-config';
+                                }
+                                index++;
+                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
+                            }
+                        });
+                } else {
+                    var deviceList = deviceFilter.deviceList;
+                    getDevices(deviceList).then(
+                        function success(devices) {
+                            if (devices && devices.length > 0) {
+                                var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
+                                resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
+                                resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
+                                index++;
+                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
+                            } else {
+                                if (!resolution.error) {
+                                    resolution.error = 'dashboard.invalid-aliases-config';
+                                }
+                                index++;
+                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
+                            }
+                        },
+                        function fail() {
+                            if (!resolution.error) {
+                                resolution.error = 'dashboard.invalid-aliases-config';
+                            }
+                            index++;
+                            processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
+                        }
+                    );
+                }
+            }
+        } else {
+            deferred.resolve(resolution);
+        }
+    }
+
+    function processDeviceAliases(deviceAliases) {
+        var deferred = $q.defer();
+        var resolution = {
+            aliasesInfo: {
+                deviceAliases: {},
+                deviceAliasesInfo: {}
+            }
+        };
+        var aliasIds = [];
+        if (deviceAliases) {
+            for (var aliasId in deviceAliases) {
+                aliasIds.push(aliasId);
+            }
+        }
+        processDeviceAlias(0, aliasIds, deviceAliases, resolution, deferred);
+        return deferred.promise;
+    }
+
+    function checkDeviceAlias(deviceAlias) {
+        var deferred = $q.defer();
+        var deviceFilter;
+        if (deviceAlias.deviceId) {
+            deviceFilter = {
+                useFilter: false,
+                deviceNameFilter: '',
+                deviceList: [deviceAlias.deviceId]
+            }
+        } else {
+            deviceFilter = deviceAlias.deviceFilter;
+        }
+        var promise;
+        if (deviceFilter.useFilter) {
+            var deviceNameFilter = deviceFilter.deviceNameFilter;
+            promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
+        } else {
+            var deviceList = deviceFilter.deviceList;
+            promise = getDevices(deviceList);
+        }
+        promise.then(
+            function success(devices) {
+                if (devices && devices.length > 0) {
+                    deferred.resolve(true);
+                } else {
+                    deferred.resolve(false);
+                }
+            },
+            function fail() {
+                deferred.resolve(false);
+            }
+        );
+        return deferred.promise;
+    }
+
     function saveDevice(device) {
         var deferred = $q.defer();
         var url = '/api/device';
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index 1141add..101ff51 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -51,7 +51,7 @@ function Dashboard() {
         scope: true,
         bindToController: {
             widgets: '=',
-            deviceAliasList: '=',
+            aliasesInfo: '=',
             dashboardTimewindow: '=?',
             columns: '=',
             margins: '=',
@@ -274,8 +274,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
         $scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
     });
 
-    $scope.$watch('vm.deviceAliasList', function () {
-        $scope.$broadcast('deviceAliasListChanged', vm.deviceAliasList);
+    $scope.$watch('vm.aliasesInfo.deviceAliases', function () {
+        $scope.$broadcast('deviceAliasListChanged', vm.aliasesInfo);
     }, true);
 
     $scope.$on('gridster-resized', function (event, sizes, theGridster) {
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index c0f815d..ef03145 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -90,7 +90,7 @@
 									<div flex tb-widget
 										 locals="{ visibleRect: vm.visibleRect,
 										 widget: widget,
-										 deviceAliasList: vm.deviceAliasList,
+										 aliasesInfo: vm.aliasesInfo,
 										 isEdit: vm.isEdit,
 										 stDiff: vm.stDiff,
 										 dashboardTimewindow: vm.dashboardTimewindow,
diff --git a/ui/src/app/components/device-filter.directive.js b/ui/src/app/components/device-filter.directive.js
new file mode 100644
index 0000000..ecf12cd
--- /dev/null
+++ b/ui/src/app/components/device-filter.directive.js
@@ -0,0 +1,217 @@
+/*
+ * 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 deviceFilterTemplate from './device-filter.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import './device-filter.scss';
+
+export default angular.module('thingsboard.directives.deviceFilter', [])
+    .directive('tbDeviceFilter', DeviceFilter)
+    .name;
+
+/*@ngInject*/
+function DeviceFilter($compile, $templateCache, $q, deviceService) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+
+        var template = $templateCache.get(deviceFilterTemplate);
+        element.html(template);
+
+        scope.ngModelCtrl = ngModelCtrl;
+
+        scope.fetchDevices = function(searchText, limit) {
+            var pageLink = {limit: limit, textSearch: searchText};
+
+            var deferred = $q.defer();
+
+            deviceService.getTenantDevices(pageLink).then(function success(result) {
+                deferred.resolve(result.data);
+            }, function fail() {
+                deferred.reject();
+            });
+
+            return deferred.promise;
+        }
+
+        scope.updateValidity = function() {
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                var valid;
+                if (value.useFilter) {
+                    ngModelCtrl.$setValidity('deviceList', true);
+                    if (angular.isDefined(value.deviceNameFilter) && value.deviceNameFilter.length > 0) {
+                        ngModelCtrl.$setValidity('deviceNameFilter', true);
+                        valid = angular.isDefined(scope.model.matchingFilterDevice) && scope.model.matchingFilterDevice != null;
+                        ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', valid);
+                    } else {
+                        ngModelCtrl.$setValidity('deviceNameFilter', false);
+                    }
+                } else {
+                    ngModelCtrl.$setValidity('deviceNameFilter', true);
+                    ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', true);
+                    valid = angular.isDefined(value.deviceList) && value.deviceList.length > 0;
+                    ngModelCtrl.$setValidity('deviceList', valid);
+                }
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            destroyWatchers();
+            scope.model = {
+                useFilter: false,
+                deviceList: [],
+                deviceNameFilter: ''
+            }
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                var model = scope.model;
+                model.useFilter = value.useFilter === true ? true: false;
+                model.deviceList = [];
+                model.deviceNameFilter = value.deviceNameFilter || '';
+                processDeviceNameFilter(model.deviceNameFilter).then(
+                    function(device) {
+                        scope.model.matchingFilterDevice = device;
+                        if (value.deviceList && value.deviceList.length > 0) {
+                            deviceService.getDevices(value.deviceList).then(function (devices) {
+                                model.deviceList = devices;
+                                updateMatchingDevice();
+                                initWatchers();
+                            });
+                        } else {
+                            updateMatchingDevice();
+                            initWatchers();
+                        }
+                    }
+                )
+            }
+        }
+
+        function updateMatchingDevice() {
+            if (scope.model.useFilter) {
+                scope.model.matchingDevice = scope.model.matchingFilterDevice;
+            } else {
+                if (scope.model.deviceList && scope.model.deviceList.length > 0) {
+                    scope.model.matchingDevice = scope.model.deviceList[0];
+                } else {
+                    scope.model.matchingDevice = null;
+                }
+            }
+        }
+
+        function processDeviceNameFilter(deviceNameFilter) {
+            var deferred = $q.defer();
+            if (angular.isDefined(deviceNameFilter) && deviceNameFilter.length > 0) {
+                scope.fetchDevices(deviceNameFilter, 1).then(function (devices) {
+                    if (devices && devices.length > 0) {
+                        deferred.resolve(devices[0]);
+                    } else {
+                        deferred.resolve(null);
+                    }
+                });
+            } else {
+                deferred.resolve(null);
+            }
+            return deferred.promise;
+        }
+
+        function destroyWatchers() {
+            if (scope.deviceListDeregistration) {
+                scope.deviceListDeregistration();
+                scope.deviceListDeregistration = null;
+            }
+            if (scope.useFilterDeregistration) {
+                scope.useFilterDeregistration();
+                scope.useFilterDeregistration = null;
+            }
+            if (scope.deviceNameFilterDeregistration) {
+                scope.deviceNameFilterDeregistration();
+                scope.deviceNameFilterDeregistration = null;
+            }
+            if (scope.matchingDeviceDeregistration) {
+                scope.matchingDeviceDeregistration();
+                scope.matchingDeviceDeregistration = null;
+            }
+        }
+
+        function initWatchers() {
+            scope.deviceListDeregistration = scope.$watch('model.deviceList', function () {
+                if (ngModelCtrl.$viewValue) {
+                    var value = ngModelCtrl.$viewValue;
+                    value.deviceList = [];
+                    if (scope.model.deviceList && scope.model.deviceList.length > 0) {
+                        for (var i in scope.model.deviceList) {
+                            value.deviceList.push(scope.model.deviceList[i].id.id);
+                        }
+                    }
+                    updateMatchingDevice();
+                    ngModelCtrl.$setViewValue(value);
+                    scope.updateValidity();
+                }
+            }, true);
+            scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
+                if (ngModelCtrl.$viewValue) {
+                    var value = ngModelCtrl.$viewValue;
+                    value.useFilter = scope.model.useFilter;
+                    updateMatchingDevice();
+                    ngModelCtrl.$setViewValue(value);
+                    scope.updateValidity();
+                }
+            });
+            scope.deviceNameFilterDeregistration = scope.$watch('model.deviceNameFilter', function (newNameFilter, prevNameFilter) {
+                if (ngModelCtrl.$viewValue) {
+                    if (!angular.equals(newNameFilter, prevNameFilter)) {
+                        var value = ngModelCtrl.$viewValue;
+                        value.deviceNameFilter = scope.model.deviceNameFilter;
+                        processDeviceNameFilter(value.deviceNameFilter).then(
+                            function(device) {
+                                scope.model.matchingFilterDevice = device;
+                                updateMatchingDevice();
+                                ngModelCtrl.$setViewValue(value);
+                                scope.updateValidity();
+                            }
+                        );
+                    }
+                }
+            });
+
+            scope.matchingDeviceDeregistration = scope.$watch('model.matchingDevice', function (newMatchingDevice, prevMatchingDevice) {
+                if (!angular.equals(newMatchingDevice, prevMatchingDevice)) {
+                    if (scope.onMatchingDeviceChange) {
+                        scope.onMatchingDeviceChange({device: newMatchingDevice});
+                    }
+                }
+            });
+        }
+
+        $compile(element.contents())(scope);
+
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            isEdit: '=',
+            onMatchingDeviceChange: '&'
+        }
+    };
+
+}
diff --git a/ui/src/app/components/device-filter.scss b/ui/src/app/components/device-filter.scss
new file mode 100644
index 0000000..ce7517f
--- /dev/null
+++ b/ui/src/app/components/device-filter.scss
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-device-filter {
+  #device_list_chips {
+    .md-chips {
+      padding-bottom: 1px;
+    }
+  }
+  .device-name-filter-input {
+    margin-top: 10px;
+    margin-bottom: 0px;
+    .md-errors-spacer {
+      min-height: 0px;
+    }
+  }
+  .tb-filter-switch {
+    padding-left: 10px;
+    .filter-switch {
+      margin: 0;
+    }
+    .filter-label {
+      margin: 5px 0;
+    }
+  }
+  .tb-error-messages {
+    margin-top: -11px;
+    height: 35px;
+    .tb-error-message {
+      padding-left: 1px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/components/device-filter.tpl.html b/ui/src/app/components/device-filter.tpl.html
new file mode 100644
index 0000000..c96066c
--- /dev/null
+++ b/ui/src/app/components/device-filter.tpl.html
@@ -0,0 +1,67 @@
+<!--
+
+    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.
+
+-->
+<section layout='column' class="tb-device-filter">
+    <section layout='row'>
+        <section layout="column" flex ng-show="!model.useFilter">
+            <md-chips flex
+                      id="device_list_chips"
+                      ng-required="!useFilter"
+                      ng-model="model.deviceList" md-autocomplete-snap
+                      md-require-match="true">
+                <md-autocomplete
+                        md-no-cache="true"
+                        id="device"
+                        md-selected-item="selectedDevice"
+                        md-search-text="deviceSearchText"
+                        md-items="item in fetchDevices(deviceSearchText, 10)"
+                        md-item-text="item.name"
+                        md-min-length="0"
+                        placeholder="{{ 'device.device-list' | translate }}">
+                        <md-item-template>
+                            <span md-highlight-text="deviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
+                        </md-item-template>
+                        <md-not-found>
+                            <span translate translate-values='{ device: deviceSearchText }'>device.no-devices-matching</span>
+                        </md-not-found>
+                </md-autocomplete>
+                <md-chip-template>
+                    <span>
+                      <strong>{{$chip.name}}</strong>
+                    </span>
+                </md-chip-template>
+            </md-chips>
+        </section>
+        <section layout="row" flex ng-show="model.useFilter">
+            <md-input-container flex class="device-name-filter-input">
+                <label translate>device.name-starts-with</label>
+                <input ng-model="model.deviceNameFilter" aria-label="{{ 'device.name-starts-with' | translate }}">
+            </md-input-container>
+        </section>
+        <section class="tb-filter-switch" layout="column" layout-align="center center">
+            <label class="tb-small filter-label" translate>device.use-device-name-filter</label>
+            <md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
+            </md-switch>
+        </section>
+    </section>
+    <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+        <div translate ng-message="deviceList" class="tb-error-message">device.device-list-empty</div>
+        <div translate ng-message="deviceNameFilter" class="tb-error-message">device.device-name-filter-required</div>
+        <div translate translate-values='{ device: model.deviceNameFilter }' ng-message="deviceNameFilterDeviceMatch"
+             class="tb-error-message">device.device-name-filter-no-device-matched</div>
+    </div>
+</section>
\ No newline at end of file
diff --git a/ui/src/app/components/legend.directive.js b/ui/src/app/components/legend.directive.js
index e4026f1..fba58e4 100644
--- a/ui/src/app/components/legend.directive.js
+++ b/ui/src/app/components/legend.directive.js
@@ -44,8 +44,10 @@ function Legend($compile, $templateCache, types) {
         scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
             scope.legendConfig.position === types.position.top.value;
 
-        scope.$on('legendDataUpdated', function () {
-            scope.$digest();
+        scope.$on('legendDataUpdated', function (event, apply) {
+            if (apply) {
+                scope.$digest();
+            }
         });
 
         scope.toggleHideData = function(index) {
@@ -62,15 +64,14 @@ function Legend($compile, $templateCache, types) {
      data: []
 
      key: {
-     label: '',
-     color: ''
-     dataIndex: 0
+       dataKey: dataKey,
+       dataIndex: 0
      }
      data: {
-     min: null,
-     max: null,
-     avg: null,
-     total: null
+       min: null,
+       max: null,
+       avg: null,
+       total: null
      }
      };*/
 
diff --git a/ui/src/app/components/legend.tpl.html b/ui/src/app/components/legend.tpl.html
index fe5de42..8350320 100644
--- a/ui/src/app/components/legend.tpl.html
+++ b/ui/src/app/components/legend.tpl.html
@@ -27,11 +27,11 @@
     </thead>
     <tbody>
         <tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys">
-            <td><span class="tb-legend-line"  ng-style="{backgroundColor: legendKey.color}"></span></td>
+            <td><span class="tb-legend-line"  ng-style="{backgroundColor: legendKey.dataKey.color}"></span></td>
             <td class="tb-legend-label"
                 ng-click="toggleHideData(legendKey.dataIndex)"
                 ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }">
-                {{ legendKey.label }}
+                {{ legendKey.dataKey.label }}
             </td>
             <td class="tb-legend-value" ng-if="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td>
             <td class="tb-legend-value" ng-if="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td>
diff --git a/ui/src/app/components/timeinterval.directive.js b/ui/src/app/components/timeinterval.directive.js
index eaaa4a1..8f7e4a4 100644
--- a/ui/src/app/components/timeinterval.directive.js
+++ b/ui/src/app/components/timeinterval.directive.js
@@ -84,6 +84,13 @@ function Timeinterval($compile, $templateCache, timeService) {
             scope.rendered = true;
         }
 
+        function calculateIntervalMs() {
+            return (scope.days * 86400 +
+                scope.hours * 3600 +
+                scope.mins * 60 +
+                scope.secs) * 1000;
+        }
+
         scope.updateView = function () {
             if (!scope.rendered) {
                 return;
@@ -92,11 +99,11 @@ function Timeinterval($compile, $templateCache, timeService) {
             var intervalMs;
             if (!scope.advanced) {
                 intervalMs = scope.intervalMs;
+                if (!intervalMs || isNaN(intervalMs)) {
+                    intervalMs = calculateIntervalMs();
+                }
             } else {
-                intervalMs = (scope.days * 86400 +
-                scope.hours * 3600 +
-                scope.mins * 60 +
-                scope.secs) * 1000;
+                intervalMs = calculateIntervalMs();
             }
             if (!isNaN(intervalMs) && intervalMs > 0) {
                 value = intervalMs;
@@ -135,12 +142,13 @@ function Timeinterval($compile, $templateCache, timeService) {
         scope.$watch('advanced', function (newAdvanced, prevAdvanced) {
             if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) {
                 if (!scope.advanced) {
-                    scope.intervalMs = (scope.days * 86400 +
-                        scope.hours * 3600 +
-                        scope.mins * 60 +
-                        scope.secs) * 1000;
+                    scope.intervalMs = calculateIntervalMs();
                 } else {
-                    scope.setIntervalMs(scope.intervalMs);
+                    var intervalMs = scope.intervalMs;
+                    if (!intervalMs || isNaN(intervalMs)) {
+                        intervalMs = calculateIntervalMs();
+                    }
+                    scope.setIntervalMs(intervalMs);
                 }
                 scope.updateView();
             }
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index 59f7b0e..bd75d0c 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -21,7 +21,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
 /*@ngInject*/
 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
                                          datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
-                                         dashboardTimewindowApi, widget, deviceAliasList, widgetType) {
+                                         dashboardTimewindowApi, widget, aliasesInfo, widgetType) {
 
     var vm = this;
 
@@ -45,6 +45,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
     var subscriptionTimewindow = null;
     var dataUpdateCaf = null;
 
+    var varsRegex = /\$\{([^\}]*)\}/g;
+
     /*
      *   data = array of datasourceData
      *   datasourceData = {
@@ -68,7 +70,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         settings: widget.config.settings,
         units: widget.config.units || '',
         decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
-        datasources: widget.config.datasources,
+        datasources: angular.copy(widget.config.datasources),
         data: [],
         hiddenData: [],
         timeWindow: {
@@ -320,10 +322,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
              $scope.legendConfig.showTotal === true);
 
         if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
-            for (var i in widget.config.datasources) {
-                var datasource = angular.copy(widget.config.datasources[i]);
+            for (var i in widgetContext.datasources) {
+                var datasource = widgetContext.datasources[i];
                 for (var a in datasource.dataKeys) {
                     var dataKey = datasource.dataKeys[a];
+                    dataKey.pattern = angular.copy(dataKey.label);
                     var datasourceData = {
                         datasource: datasource,
                         dataKey: dataKey,
@@ -333,8 +336,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
                     widgetContext.hiddenData.push({data: []});
                     if ($scope.displayLegend) {
                         var legendKey = {
-                            label: dataKey.label,
-                            color: dataKey.color,
+                            dataKey: dataKey,
                             dataIndex: Number(i) + Number(a)
                         };
                         $scope.legendData.keys.push(legendKey);
@@ -367,8 +369,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         } else if (widget.type === types.widgetType.rpc.value) {
             if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
                 targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
-                if (deviceAliasList[targetDeviceAliasId]) {
-                    targetDeviceId = deviceAliasList[targetDeviceAliasId].deviceId;
+                if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
+                    targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
                 }
             }
             if (targetDeviceId) {
@@ -402,13 +404,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
             onMobileModeChanged(newIsMobile);
         });
 
-        $scope.$on('deviceAliasListChanged', function (event, newDeviceAliasList) {
-            deviceAliasList = newDeviceAliasList;
+        $scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) {
+            aliasesInfo = newAliasesInfo;
             if (widget.type === types.widgetType.rpc.value) {
                 if (targetDeviceAliasId) {
                     var deviceId = null;
-                    if (deviceAliasList[targetDeviceAliasId]) {
-                        deviceId = deviceAliasList[targetDeviceAliasId].deviceId;
+                    if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
+                        deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
                     }
                     if (!angular.equals(deviceId, targetDeviceId)) {
                         targetDeviceId = deviceId;
@@ -609,7 +611,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
             currentData.data = sourceData.data;
             onDataUpdated();
             if ($scope.caulculateLegendData) {
-                updateLegend(datasourceIndex + dataKeyIndex, sourceData.data);
+                updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
             }
         }
         if (apply) {
@@ -617,7 +619,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
     }
 
-    function updateLegend(dataIndex, data) {
+    function updateLegend(dataIndex, data, apply) {
         var legendKeyData = $scope.legendData.data[dataIndex];
         if ($scope.legendConfig.showMin) {
             legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units);
@@ -631,7 +633,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         if ($scope.legendConfig.showTotal) {
             legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units);
         }
-        $scope.$broadcast('legendDataUpdated');
+        $scope.$broadcast('legendDataUpdated', apply !== false);
     }
 
     function isNumeric(val) {
@@ -707,9 +709,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
                 var deviceId = null;
                 var aliasName = null;
                 if (listener.datasource.type === types.datasourceType.device) {
-                    if (deviceAliasList[listener.datasource.deviceAliasId]) {
-                        deviceId = deviceAliasList[listener.datasource.deviceAliasId].deviceId;
-                        aliasName = deviceAliasList[listener.datasource.deviceAliasId].alias;
+                    if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
+                        deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
+                        aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
                     }
                     if (!angular.equals(deviceId, listener.deviceId) ||
                         !angular.equals(aliasName, listener.datasource.name)) {
@@ -756,6 +758,23 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
     }
 
+    function updateDataKeyLabel(dataKey, deviceName, aliasName) {
+        var pattern = dataKey.pattern;
+        var label = dataKey.pattern;
+        var match = varsRegex.exec(pattern);
+        while (match !== null) {
+            var variable = match[0];
+            var variableName = match[1];
+            if (variableName === 'deviceName') {
+                label = label.split(variable).join(deviceName);
+            } else if (variableName === 'aliasName') {
+                label = label.split(variable).join(aliasName);
+            }
+            match = varsRegex.exec(pattern);
+        }
+        dataKey.label = label;
+    }
+
     function subscribe() {
         if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
             notifyDataLoading();
@@ -767,13 +786,25 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
                 }
             }
             var index = 0;
-            for (var i in widget.config.datasources) {
-                var datasource = widget.config.datasources[i];
+            for (var i in widgetContext.datasources) {
+                var datasource = widgetContext.datasources[i];
                 var deviceId = null;
                 if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
-                    if (deviceAliasList[datasource.deviceAliasId]) {
-                        deviceId = deviceAliasList[datasource.deviceAliasId].deviceId;
-                        datasource.name = deviceAliasList[datasource.deviceAliasId].alias;
+                    if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
+                        deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
+                        datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
+                        var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
+                        var deviceName = '';
+                        var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
+                        for (var d=0;d<devicesInfo.length;d++) {
+                            if (devicesInfo[d].id === deviceId) {
+                                deviceName = devicesInfo[d].name;
+                                break;
+                            }
+                        }
+                        for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
+                            updateDataKeyLabel(datasource.dataKeys[dk], deviceName, aliasName);
+                        }
                     }
                 } else {
                     datasource.name = types.datasourceType.function;
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index ab0d6d4..e9ceaad 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -20,11 +20,12 @@ import deviceAliasesTemplate from './device-aliases.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, widget, widgetInfo) {
+export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
 
     var vm = this;
 
     vm.dashboard = dashboard;
+    vm.aliasesInfo = aliasesInfo;
     vm.widget = widget;
     vm.widgetInfo = widgetInfo;
 
@@ -78,19 +79,19 @@ export default function AddWidgetController($scope, widgetService, deviceService
     }
 
     function cancel () {
-        $mdDialog.cancel();
+        $mdDialog.cancel({aliasesInfo: vm.aliasesInfo});
     }
 
     function add () {
         if ($scope.theForm.$valid) {
             $scope.theForm.$setPristine();
             vm.widget.config = vm.widgetConfig;
-            $mdDialog.hide(vm.widget);
+            $mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo});
         }
     }
 
     function fetchDeviceKeys (deviceAliasId, query, type) {
-        var deviceAlias = vm.dashboard.configuration.deviceAliases[deviceAliasId];
+        var deviceAlias = vm.aliasesInfo.deviceAliases[deviceAliasId];
         if (deviceAlias && deviceAlias.deviceId) {
             return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
         } else {
@@ -101,7 +102,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
     function createDeviceAlias (event, alias) {
 
         var deferred = $q.defer();
-        var singleDeviceAlias = {id: null, alias: alias, deviceId: null};
+        var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
 
         $mdDialog.show({
             controller: 'DeviceAliasesController',
@@ -111,7 +112,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
                 config: {
                     deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
                     widgets: null,
-                    isSingleDevice: true,
+                    isSingleDeviceAlias: true,
                     singleDeviceAlias: singleDeviceAlias
                 }
             },
@@ -121,8 +122,15 @@ export default function AddWidgetController($scope, widgetService, deviceService
             targetEvent: event
         }).then(function (singleDeviceAlias) {
             vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
-                { alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId };
-            deferred.resolve(singleDeviceAlias);
+                { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
+            deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases).then(
+                function(resolution) {
+                    if (!resolution.error) {
+                        vm.aliasesInfo = resolution.aliasesInfo;
+                    }
+                    deferred.resolve(singleDeviceAlias);
+                }
+            );
         }, function () {
             deferred.reject();
         });
diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
index b6e773e..4e6db50 100644
--- a/ui/src/app/dashboard/add-widget.tpl.html
+++ b/ui/src/app/dashboard/add-widget.tpl.html
@@ -37,7 +37,7 @@
                                       ng-model="vm.widgetConfig"
                                       widget-settings-schema="vm.settingsSchema"
                                       datakey-settings-schema="vm.dataKeySettingsSchema"
-                                      device-aliases="vm.dashboard.configuration.deviceAliases"
+                                      device-aliases="vm.aliasesInfo.deviceAliases"
                                       functions-only="vm.functionsOnly"
                                       fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
                                       on-create-device-alias="vm.createDeviceAlias(event, alias)"
diff --git a/ui/src/app/dashboard/aliases-device-select.directive.js b/ui/src/app/dashboard/aliases-device-select.directive.js
new file mode 100644
index 0000000..021e8bb
--- /dev/null
+++ b/ui/src/app/dashboard/aliases-device-select.directive.js
@@ -0,0 +1,153 @@
+/*
+ * 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 './aliases-device-select.scss';
+
+import $ from 'jquery';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import aliasesDeviceSelectButtonTemplate from './aliases-device-select-button.tpl.html';
+import aliasesDeviceSelectPanelTemplate from './aliases-device-select-panel.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+/*@ngInject*/
+export default function AliasesDeviceSelectDirective($compile, $templateCache, types, $mdPanel, $document, $translate) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+
+        /* tbAliasesDeviceSelect (ng-model)
+         * {
+         *    "aliasId": {
+         *        alias: alias,
+         *        deviceId: deviceId
+         *    }
+         * }
+         */
+
+        var template = $templateCache.get(aliasesDeviceSelectButtonTemplate);
+
+        scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top';
+
+        element.html(template);
+
+        scope.openEditMode = function (event) {
+            if (scope.disabled) {
+                return;
+            }
+            var position;
+            var panelHeight = 250;
+            var panelWidth = 300;
+            var offset = element[0].getBoundingClientRect();
+            var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
+            var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
+            var yPosition;
+            var xPosition;
+            if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
+                yPosition = $mdPanel.yPosition.ABOVE;
+            } else {
+                yPosition = $mdPanel.yPosition.BELOW;
+            }
+            if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
+                xPosition = $mdPanel.xPosition.ALIGN_END;
+            } else {
+                xPosition = $mdPanel.xPosition.ALIGN_START;
+            }
+            position = $mdPanel.newPanelPosition()
+                .relativeTo(element)
+                .addPanelPosition(xPosition, yPosition);
+            var config = {
+                attachTo: angular.element($document[0].body),
+                controller: 'AliasesDeviceSelectPanelController',
+                controllerAs: 'vm',
+                templateUrl: aliasesDeviceSelectPanelTemplate,
+                panelClass: 'tb-aliases-device-select-panel',
+                position: position,
+                fullscreen: false,
+                locals: {
+                    'deviceAliases': angular.copy(scope.model),
+                    'deviceAliasesInfo': scope.deviceAliasesInfo,
+                    'onDeviceAliasesUpdate': function (deviceAliases) {
+                        scope.model = deviceAliases;
+                        scope.updateView();
+                    }
+                },
+                openFrom: event,
+                clickOutsideToClose: true,
+                escapeToClose: true,
+                focusOnOpen: false
+            };
+            $mdPanel.open(config);
+        }
+
+        scope.updateView = function () {
+            var value = angular.copy(scope.model);
+            ngModelCtrl.$setViewValue(value);
+            updateDisplayValue();
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                scope.model = angular.copy(value);
+                updateDisplayValue();
+            }
+        }
+
+        function updateDisplayValue() {
+            var displayValue;
+            var singleValue = true;
+            var currentAliasId;
+            for (var aliasId in scope.model) {
+                if (!currentAliasId) {
+                    currentAliasId = aliasId;
+                } else {
+                    singleValue = false;
+                    break;
+                }
+            }
+            if (singleValue) {
+                var deviceId = scope.model[currentAliasId].deviceId;
+                var devicesInfo = scope.deviceAliasesInfo[currentAliasId];
+                for (var i=0;i<devicesInfo.length;i++) {
+                    if (devicesInfo[i].id === deviceId) {
+                        displayValue = devicesInfo[i].name;
+                        break;
+                    }
+                }
+            } else {
+                displayValue = $translate.instant('device.devices');
+            }
+            scope.displayValue = displayValue;
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            deviceAliasesInfo:'='
+        },
+        link: linker
+    };
+
+}
+
+/* eslint-enable angular/angularelement */
\ No newline at end of file
diff --git a/ui/src/app/dashboard/aliases-device-select.scss b/ui/src/app/dashboard/aliases-device-select.scss
new file mode 100644
index 0000000..ebd7cd2
--- /dev/null
+++ b/ui/src/app/dashboard/aliases-device-select.scss
@@ -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-panel {
+  &.tb-aliases-device-select-panel {
+    position: absolute;
+  }
+}
+
+.tb-aliases-device-select-panel {
+  max-height: 250px;
+  min-width: 300px;
+  background: white;
+  border-radius: 4px;
+  box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
+  0 13px 19px 2px rgba(0, 0, 0, 0.14),
+  0 5px 24px 4px rgba(0, 0, 0, 0.12);
+  overflow-x: hidden;
+  overflow-y: auto;
+  md-content {
+    background-color: #fff;
+  }
+}
+
+.tb-aliases-device-select {
+  span {
+    pointer-events: all;
+    cursor: pointer;
+  }
+}
diff --git a/ui/src/app/dashboard/aliases-device-select-button.tpl.html b/ui/src/app/dashboard/aliases-device-select-button.tpl.html
new file mode 100644
index 0000000..e867982
--- /dev/null
+++ b/ui/src/app/dashboard/aliases-device-select-button.tpl.html
@@ -0,0 +1,32 @@
+<!--
+
+    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.
+
+-->
+
+<section class="tb-aliases-device-select" layout='row' layout-align="start center" ng-style="{minHeight: '32px', padding: '0 6px'}">
+    <md-button class="md-icon-button" aria-label="{{ 'dashboard.select-devices' | translate }}" ng-click="openEditMode($event)">
+        <md-tooltip md-direction="{{tooltipDirection}}">
+            {{ 'dashboard.select-devices' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'dashboard.select-devices' | translate }}" class="material-icons">devices_other</md-icon>
+    </md-button>
+    <span ng-click="openEditMode($event)">
+        <md-tooltip md-direction="{{tooltipDirection}}">
+            {{ 'dashboard.select-devices' | translate }}
+        </md-tooltip>
+        {{displayValue}}
+    </span>
+</section>
diff --git a/ui/src/app/dashboard/aliases-device-select-panel.controller.js b/ui/src/app/dashboard/aliases-device-select-panel.controller.js
new file mode 100644
index 0000000..fec6e1a
--- /dev/null
+++ b/ui/src/app/dashboard/aliases-device-select-panel.controller.js
@@ -0,0 +1,31 @@
+/*
+ * 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 AliasesDeviceSelectPanelController(mdPanelRef, $scope, types, deviceAliases, deviceAliasesInfo, onDeviceAliasesUpdate) {
+
+    var vm = this;
+    vm._mdPanelRef = mdPanelRef;
+    vm.deviceAliases = deviceAliases;
+    vm.deviceAliasesInfo = deviceAliasesInfo;
+    vm.onDeviceAliasesUpdate = onDeviceAliasesUpdate;
+
+    $scope.$watch('vm.deviceAliases', function () {
+        if (onDeviceAliasesUpdate) {
+            onDeviceAliasesUpdate(vm.deviceAliases);
+        }
+    }, true);
+}
diff --git a/ui/src/app/dashboard/aliases-device-select-panel.tpl.html b/ui/src/app/dashboard/aliases-device-select-panel.tpl.html
new file mode 100644
index 0000000..e151595
--- /dev/null
+++ b/ui/src/app/dashboard/aliases-device-select-panel.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.
+
+-->
+<md-content flex layout="column">
+    <section flex layout="column">
+        <md-content flex class="md-padding" layout="column">
+            <div flex layout="row" ng-repeat="(aliasId, deviceAlias) in vm.deviceAliases">
+                <md-input-container flex>
+                    <label>{{deviceAlias.alias}}</label>
+                    <md-select ng-model="vm.deviceAliases[aliasId].deviceId">
+                        <md-option ng-repeat="deviceInfo in vm.deviceAliasesInfo[aliasId]" ng-value="deviceInfo.id">
+                            {{deviceInfo.name}}
+                        </md-option>
+                    </md-select>
+                </md-input-container>
+            </div>
+        </md-content>
+    </section>
+</md-content>
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index a9626be..b2ec482 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
 
 /*@ngInject*/
 export default function DashboardController(types, widgetService, userService,
-                                            dashboardService, timeService, itembuffer, importExport, hotkeys, $window, $rootScope,
+                                            dashboardService, timeService, deviceService, itembuffer, importExport, hotkeys, $window, $rootScope,
                                             $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
 
     var user = userService.getCurrentUser();
@@ -82,6 +82,8 @@ export default function DashboardController(types, widgetService, userService,
     vm.loadDashboard = loadDashboard;
     vm.getServerTimeDiff = getServerTimeDiff;
     vm.noData = noData;
+    vm.dashboardConfigurationError = dashboardConfigurationError;
+    vm.showDashboardToolbar = showDashboardToolbar;
     vm.onAddWidgetClosed = onAddWidgetClosed;
     vm.onEditWidgetClosed = onEditWidgetClosed;
     vm.openDeviceAliases = openDeviceAliases;
@@ -212,9 +214,21 @@ export default function DashboardController(types, widgetService, userService,
                     if (angular.isUndefined(vm.dashboard.configuration.timewindow)) {
                         vm.dashboard.configuration.timewindow = timeService.defaultTimewindow();
                     }
-                    vm.dashboardConfiguration = vm.dashboard.configuration;
-                    vm.widgets = vm.dashboard.configuration.widgets;
-                    deferred.resolve();
+                    deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
+                        .then(
+                            function(resolution) {
+                                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();
+                                }
+                            }
+                        );
                 }, function fail(e) {
                     deferred.reject(e);
                 });
@@ -245,7 +259,15 @@ export default function DashboardController(types, widgetService, userService,
     }
 
     function noData() {
-        return vm.dashboardInitComplete && vm.widgets.length == 0;
+        return vm.dashboardInitComplete && !vm.configurationError && vm.widgets.length == 0;
+    }
+
+    function dashboardConfigurationError() {
+        return vm.dashboardInitComplete && vm.configurationError;
+    }
+
+    function showDashboardToolbar() {
+        return vm.dashboardInitComplete && !vm.configurationError;
     }
 
     function openDeviceAliases($event) {
@@ -257,7 +279,7 @@ export default function DashboardController(types, widgetService, userService,
                 config: {
                     deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
                     widgets: vm.widgets,
-                    isSingleDevice: false,
+                    isSingleDeviceAlias: false,
                     singleDeviceAlias: null
                 }
             },
@@ -267,6 +289,7 @@ export default function DashboardController(types, widgetService, userService,
             targetEvent: $event
         }).then(function (deviceAliases) {
             vm.dashboard.configuration.deviceAliases = deviceAliases;
+            deviceAliasesUpdated();
         }, function () {
         });
     }
@@ -331,7 +354,7 @@ export default function DashboardController(types, widgetService, userService,
 
     function importWidget($event) {
         $event.stopPropagation();
-        importExport.importWidget($event, vm.dashboard);
+        importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated);
     }
 
     function widgetMouseDown($event, widget) {
@@ -433,7 +456,7 @@ export default function DashboardController(types, widgetService, userService,
 
     function pasteWidget($event) {
         var pos = vm.dashboardContainer.getEventGridPosition($event);
-        itembuffer.pasteWidget(vm.dashboard, pos);
+        itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated);
     }
 
     function prepareWidgetContextMenu() {
@@ -570,7 +593,7 @@ export default function DashboardController(types, widgetService, userService,
                     controller: 'AddWidgetController',
                     controllerAs: 'vm',
                     templateUrl: addWidgetTemplate,
-                    locals: {dashboard: vm.dashboard, widget: newWidget, widgetInfo: widgetTypeInfo},
+                    locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo},
                     parent: angular.element($document[0].body),
                     fullscreen: true,
                     skipHide: true,
@@ -579,7 +602,9 @@ export default function DashboardController(types, widgetService, userService,
                         var w = angular.element($window);
                         w.triggerHandler('resize');
                     }
-                }).then(function (widget) {
+                }).then(function (result) {
+                    var widget = result.widget;
+                    vm.aliasesInfo = result.aliasesInfo;
                     var columns = 24;
                     if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
                         columns = vm.dashboard.configuration.gridSettings.columns;
@@ -590,7 +615,8 @@ export default function DashboardController(types, widgetService, userService,
                         widget.sizeY *= ratio;
                     }
                     vm.widgets.push(widget);
-                }, function () {
+                }, function (rejection) {
+                    vm.aliasesInfo = rejection.aliasesInfo;
                 });
             }
         );
@@ -634,6 +660,7 @@ export default function DashboardController(types, widgetService, userService,
                     vm.dashboard = vm.prevDashboard;
                     vm.widgets = vm.dashboard.configuration.widgets;
                     vm.dashboardConfiguration = vm.dashboard.configuration;
+                    deviceAliasesUpdated();
                 }
             }
         }
@@ -648,6 +675,31 @@ export default function DashboardController(types, widgetService, userService,
         notifyDashboardUpdated();
     }
 
+    function showAliasesResolutionError(error) {
+        var alert = $mdDialog.alert()
+            .parent(angular.element($document[0].body))
+            .clickOutsideToClose(true)
+            .title($translate.instant('dashboard.alias-resolution-error-title'))
+            .htmlContent($translate.instant(error))
+            .ariaLabel($translate.instant('dashboard.alias-resolution-error-title'))
+            .ok($translate.instant('action.close'))
+        alert._options.skipHide = true;
+        alert._options.fullscreen = true;
+
+        $mdDialog.show(alert);
+    }
+
+    function deviceAliasesUpdated() {
+        deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
+            .then(
+                function(resolution) {
+                    if (resolution.aliasesInfo) {
+                        vm.aliasesInfo = resolution.aliasesInfo;
+                    }
+                }
+            );
+    }
+
     function notifyDashboardUpdated() {
         if (vm.widgetEditMode) {
             var parentScope = $window.parent.angular.element($window.frameElement).scope();
diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss
index 6966680..1744a53 100644
--- a/ui/src/app/dashboard/dashboard.scss
+++ b/ui/src/app/dashboard/dashboard.scss
@@ -113,13 +113,11 @@ section.tb-dashboard-toolbar {
         min-height: 36px;
         height: 36px;
         md-fab-actions {
+          font-size: 16px;
           margin-top: 0px;
           .close-action {
             margin-right: -18px;
           }
-          tb-timewindow {
-            font-size: 16px;
-          }
         }
       }
     }
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 716999e..7390485 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -23,7 +23,7 @@
                     'background-attachment': 'scroll',
                     'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
                     'background-position': '0% 0%'}">
-    <section class="tb-dashboard-toolbar"
+    <section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
              ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
         <md-fab-toolbar md-open="vm.toolbarOpened"
                         md-direction="left">
@@ -49,6 +49,11 @@
                     </md-button>
                     <tb-timewindow direction="left" tooltip-direction="bottom" aggregation ng-model="vm.dashboardConfiguration.timewindow">
                     </tb-timewindow>
+                    <tb-aliases-device-select ng-show="!vm.isEdit"
+                                              tooltip-direction="bottom"
+                                              ng-model="vm.aliasesInfo.deviceAliases"
+                                              device-aliases-info="vm.aliasesInfo.deviceAliasesInfo">
+                    </tb-aliases-device-select>
                     <md-button ng-show="vm.isEdit" aria-label="{{ 'device.aliases' | translate }}" class="md-icon-button"
                                ng-click="vm.openDeviceAliases($event)">
                         <md-tooltip md-direction="bottom">
@@ -70,17 +75,27 @@
     <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>
+            <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-class="{'tb-padded' : !vm.widgetEditMode}"
+                 style="text-transform: uppercase; display: flex; z-index: 1;"
+                 class="md-headline tb-absolute-fill">
+            <span translate>
+                dashboard.configuration-error
+            </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}">
             <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
@@ -101,7 +116,7 @@
                     widgets="vm.widgets"
                     columns="vm.dashboard.configuration.gridSettings.columns"
                     margins="vm.dashboard.configuration.gridSettings.margins"
-                    device-alias-list="vm.dashboard.configuration.deviceAliases"
+                    aliases-info="vm.aliasesInfo"
                     dashboard-timewindow="vm.dashboardConfiguration.timewindow"
                     is-edit="vm.isEdit"
                     is-mobile="vm.forceDashboardMobileMode"
@@ -139,6 +154,7 @@
             <form name="vm.widgetForm" ng-if="vm.isEditingWidget">
                 <tb-edit-widget
                         dashboard="vm.dashboard"
+                        aliases-info="vm.aliasesInfo"
                         widget="vm.editingWidget"
                         the-form="vm.widgetForm">
                 </tb-edit-widget>
diff --git a/ui/src/app/dashboard/device-aliases.controller.js b/ui/src/app/dashboard/device-aliases.controller.js
index 18b3180..fc89de3 100644
--- a/ui/src/app/dashboard/device-aliases.controller.js
+++ b/ui/src/app/dashboard/device-aliases.controller.js
@@ -17,25 +17,23 @@ import './device-aliases.scss';
 
 /*@ngInject*/
 export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
-                                                types, config) {
+                                                  types, config) {
 
     var vm = this;
 
-    vm.isSingleDevice = config.isSingleDevice;
+    vm.isSingleDeviceAlias = config.isSingleDeviceAlias;
     vm.singleDeviceAlias = config.singleDeviceAlias;
     vm.deviceAliases = [];
-    vm.singleDevice = null;
-    vm.singleDeviceSearchText = '';
     vm.title = config.customTitle ? config.customTitle : 'device.aliases';
     vm.disableAdd = config.disableAdd;
     vm.aliasToWidgetsMap = {};
 
+
+    vm.onFilterDeviceChanged = onFilterDeviceChanged;
     vm.addAlias = addAlias;
-    vm.cancel = cancel;
-    vm.deviceSearchTextChanged = deviceSearchTextChanged;
-    vm.deviceChanged = deviceChanged;
-    vm.fetchDevices = fetchDevices;
     vm.removeAlias = removeAlias;
+
+    vm.cancel = cancel;
     vm.save = save;
 
     initController();
@@ -80,40 +78,46 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
             }
         }
 
-        for (aliasId in config.deviceAliases) {
-            var alias = config.deviceAliases[aliasId].alias;
-            var deviceId = config.deviceAliases[aliasId].deviceId;
-            var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
-            if (deviceId) {
-                fetchAliasDevice(deviceAlias, deviceId);
+        if (vm.isSingleDeviceAlias) {
+            if (!vm.singleDeviceAlias.deviceFilter || vm.singleDeviceAlias.deviceFilter == null) {
+                vm.singleDeviceAlias.deviceFilter = {
+                    useFilter: false,
+                    deviceNameFilter: '',
+                    deviceList: [],
+                };
             }
-            vm.deviceAliases.push(deviceAlias);
         }
-    }
-
-    function fetchDevices(searchText) {
-        var pageLink = {limit: 10, textSearch: searchText};
 
-        var deferred = $q.defer();
-
-        deviceService.getTenantDevices(pageLink).then(function success(result) {
-            deferred.resolve(result.data);
-        }, function fail() {
-            deferred.reject();
-        });
-
-        return deferred.promise;
-    }
-
-    function deviceSearchTextChanged() {
+        for (aliasId in config.deviceAliases) {
+            var deviceAlias = config.deviceAliases[aliasId];
+            var alias = deviceAlias.alias;
+            var deviceFilter;
+            if (!deviceAlias.deviceFilter) {
+                deviceFilter = {
+                    useFilter: false,
+                    deviceNameFilter: '',
+                    deviceList: [],
+                };
+                if (deviceAlias.deviceId) {
+                    deviceFilter.deviceList = [deviceAlias.deviceId];
+                } else {
+                    deviceFilter.deviceList = [];
+                }
+            } else {
+                deviceFilter = deviceAlias.deviceFilter;
+            }
+            var result = {id: aliasId, alias: alias, deviceFilter: deviceFilter, changed: true};
+            vm.deviceAliases.push(result);
+        }
     }
 
-    function deviceChanged(deviceAlias) {
-        if (deviceAlias && deviceAlias.device) {
-            if (angular.isDefined(deviceAlias.changed) && !deviceAlias.changed) {
-                deviceAlias.changed = true;
-            } else {
-                deviceAlias.alias = deviceAlias.device.name;
+    function onFilterDeviceChanged(device, deviceAlias) {
+        if (deviceAlias) {
+            if (!deviceAlias.alias || deviceAlias.alias.length == 0) {
+                deviceAlias.changed = false;
+            }
+            if (!deviceAlias.changed && device) {
+                deviceAlias.alias = device.name;
             }
         }
     }
@@ -124,7 +128,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
             aliasId = Math.max(vm.deviceAliases[a].id, aliasId);
         }
         aliasId++;
-        var deviceAlias = {id: aliasId, alias: '', device: null, searchText: ''};
+        var deviceAlias = {id: aliasId, alias: '', deviceFilter: {useFilter: false, deviceNameFilter: '', deviceList: []}, changed: false};
         vm.deviceAliases.push(deviceAlias);
     }
 
@@ -150,9 +154,6 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
 
                 $mdDialog.show(alert);
             } else {
-                for (var i = index + 1; i < vm.deviceAliases.length; i++) {
-                    vm.deviceAliases[i].changed = false;
-                }
                 vm.deviceAliases.splice(index, 1);
                 if ($scope.theForm) {
                     $scope.theForm.$setDirty();
@@ -165,6 +166,15 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
         $mdDialog.cancel();
     }
 
+    function cleanupDeviceFilter(deviceFilter) {
+        if (deviceFilter.useFilter) {
+            deviceFilter.deviceList = [];
+        } else {
+            deviceFilter.deviceNameFilter = '';
+        }
+        return deviceFilter;
+    }
+
     function save() {
 
         var deviceAliases = {};
@@ -175,9 +185,9 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
         var alias;
         var i;
 
-        if (vm.isSingleDevice) {
+        if (vm.isSingleDeviceAlias) {
             maxAliasId = 0;
-            vm.singleDeviceAlias.deviceId = vm.singleDevice.id.id;
+            vm.singleDeviceAlias.deviceFilter = cleanupDeviceFilter(vm.singleDeviceAlias.deviceFilter);
             for (i in vm.deviceAliases) {
                 aliasId = vm.deviceAliases[i].id;
                 alias = vm.deviceAliases[i].alias;
@@ -195,7 +205,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
                 alias = vm.deviceAliases[i].alias;
                 if (!uniqueAliasList[alias]) {
                     uniqueAliasList[alias] = alias;
-                    deviceAliases[aliasId] = {alias: alias, deviceId: vm.deviceAliases[i].device.id.id};
+                    deviceAliases[aliasId] = {alias: alias, deviceFilter: cleanupDeviceFilter(vm.deviceAliases[i].deviceFilter)};
                 } else {
                     valid = false;
                     break;
@@ -204,7 +214,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
         }
         if (valid) {
             $scope.theForm.$setPristine();
-            if (vm.isSingleDevice) {
+            if (vm.isSingleDeviceAlias) {
                 $mdDialog.hide(vm.singleDeviceAlias);
             } else {
                 $mdDialog.hide(deviceAliases);
@@ -214,11 +224,4 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
         }
     }
 
-    function fetchAliasDevice(deviceAlias, deviceId) {
-        deviceService.getDevice(deviceId).then(function (device) {
-            deviceAlias.device = device;
-            deviceAlias.searchText = device.name;
-        });
-    }
-
 }
diff --git a/ui/src/app/dashboard/device-aliases.scss b/ui/src/app/dashboard/device-aliases.scss
index 0f5e6da..9b1c489 100644
--- a/ui/src/app/dashboard/device-aliases.scss
+++ b/ui/src/app/dashboard/device-aliases.scss
@@ -13,20 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-.tb-alias {
-  padding: 10px 0 0 10px;
-  margin: 5px;
 
-  md-input-container {
-    margin: 0;
+.tb-aliases-dialog {
+  .md-dialog-content {
+    padding-bottom: 0px;
   }
-  md-autocomplete {
-    height: 30px;
-    md-autocomplete-wrap {
-      height: 30px;
-    }
-    input, input:not(.md-input) {
-      height: 30px;
-    }
+  .tb-alias {
+    padding: 10px 0 0 10px;
+    margin: 5px;
   }
 }
diff --git a/ui/src/app/dashboard/device-aliases.tpl.html b/ui/src/app/dashboard/device-aliases.tpl.html
index 3333e88..4b362ce 100644
--- a/ui/src/app/dashboard/device-aliases.tpl.html
+++ b/ui/src/app/dashboard/device-aliases.tpl.html
@@ -15,117 +15,80 @@
     limitations under the License.
 
 -->
-<md-dialog style="width: 700px;" aria-label="{{ vm.title | translate }}">
+<md-dialog class="tb-aliases-dialog" style="width: 700px;" aria-label="{{ vm.title | translate }}">
 	<form name="theForm" ng-submit="vm.save()">
-	    <md-toolbar>
-	      <div class="md-toolbar-tools">
-	        <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</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 ng-show="vm.isSingleDevice">
-					  <md-autocomplete					  			
-					    		ng-required="vm.isSingleDevice"
-					    		md-input-name="device_id"
-					    		ng-model="vm.singleDevice"
-								md-selected-item="vm.singleDevice"
-								md-search-text="vm.singleDeviceSearchText"
-								md-search-text-change="vm.deviceSearchTextChanged(vm.singleDevice)"
-								md-items="item in vm.fetchDevices(vm.singleDeviceSearchText)"
-								md-item-text="item.name"
-								md-min-length="0"
-								placeholder="{{ 'device.device' | translate }}">
-								<md-item-template>
-					         	    <span md-highlight-text="vm.singleDeviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
-					       		</md-item-template>
-						        <md-not-found>
-									<span translate translate-values='{ device: vm.singleDeviceSearchText }'>device.no-devices-matching</span>
-						        </md-not-found>
-						        <div ng-messages="theForm.device_id.$error">
-				      				<div translate ng-message="required">device.device-required</div>
-				    			</div>	
-					  </md-autocomplete>							
-				</div> 
-				<div ng-show="!vm.isSingleDevice" flex layout="row" layout-align="start center">
-					<span flex="5"></span>
-					<div flex layout="row" layout-align="start center" 
-						   style="padding: 0 0 0 10px; margin: 5px;">
-						   <span translate flex="40" style="min-width: 100px;">device.alias</span>
-						   <span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.device</span>
-						   <span style="min-width: 40px;"></span>
-				    </div>
-				</div>
-				<div ng-show="!vm.isSingleDevice" style="max-height: 300px; overflow: auto; padding-bottom: 20px;">
-					<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index">
-						<span flex="5">{{$index + 1}}.</span>
-						<div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
-							  <md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block">
-								<input required name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias">
-								  <div ng-messages="aliasForm.alias.$error">
-									  <div translate ng-message="required">device.alias-required</div>
-								  </div>
-							  </md-input-container>
-							  <section flex="60" layout="column">
-								  <md-autocomplete flex
-											ng-required="!vm.isSingleDevice"
-											md-input-name="device_id"
-											ng-model="deviceAlias.device"
-											md-selected-item="deviceAlias.device"
-											md-search-text="deviceAlias.searchText"
-											md-search-text-change="vm.deviceSearchTextChanged(deviceAlias)"
-											md-selected-item-change="vm.deviceChanged(deviceAlias)"
-											md-items="item in vm.fetchDevices(deviceAlias.searchText)"
-											md-item-text="item.name"
-											md-min-length="0"
-											placeholder="{{ 'device.device' | translate }}">
-											<md-item-template>
-												<span md-highlight-text="deviceAlias.searchText" md-highlight-flags="^i">{{item.name}}</span>
-											</md-item-template>
-											<md-not-found>
-												<span translate translate-values='{ device: deviceAlias.searchText }'>device.no-devices-matching</span>
-											</md-not-found>
-								  </md-autocomplete>
-								  <div class="tb-error-messages" ng-messages="aliasForm.device_id.$error">
-									<div translate ng-message="required" class="tb-error-message">device.device-required</div>
-								  </div>
-							  </section>
-							<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
-					                ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
-					          		<md-tooltip md-direction="top">
-				        						{{ 'device.remove-alias' | translate }}
-				      					</md-tooltip>
-							        <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
-							          close
-							        </md-icon>		        					
-					          </md-button>	
-				          </div>						
-					</div>					
-				</div>
-				<div ng-show="!vm.isSingleDevice && !vm.disableAdd" style="padding-bottom: 10px;">
-			         <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
-			         		<md-tooltip md-direction="top">
-			      						{{ 'device.add-alias' | translate }}
-			    			</md-tooltip>
-				        	<span translate>action.add</span>
-			         </md-button>		
-			    </div>						
-			</fieldset>	      
-	      </div>
-	    </md-dialog-content>
-	    <md-dialog-actions layout="row">
-	      <span flex></span>
-		  <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
-		  		{{ 'action.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-toolbar>
+			<div class="md-toolbar-tools">
+				<h2>{{ vm.isSingleDeviceAlias ? ('device.configure-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</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 ng-show="vm.isSingleDeviceAlias">
+						<tb-device-filter ng-model="vm.singleDeviceAlias.deviceFilter">
+						</tb-device-filter>
+					</div>
+					<div ng-show="!vm.isSingleDeviceAlias" flex layout="row" layout-align="start center">
+						<span flex="5"></span>
+						<div flex layout="row" layout-align="start center"
+							 style="padding: 0 0 0 10px; margin: 5px;">
+							<span translate flex="40" style="min-width: 100px;">device.alias</span>
+							<span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.devices</span>
+							<span style="min-width: 40px;"></span>
+						</div>
+					</div>
+					<div ng-show="!vm.isSingleDeviceAlias" style="max-height: 500px; overflow: auto; padding-bottom: 20px;">
+						<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index">
+							<span flex="5">{{$index + 1}}.</span>
+							<div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
+								<md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block">
+									<input required ng-change="deviceAlias.changed=true" name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias">
+									<div ng-messages="aliasForm.alias.$error">
+										<div translate ng-message="required">device.alias-required</div>
+									</div>
+								</md-input-container>
+								<section flex="60" layout="column">
+									<tb-device-filter style="padding-left: 10px;"
+													  ng-model="deviceAlias.deviceFilter"
+													  on-matching-device-change="vm.onFilterDeviceChanged(device, deviceAlias)">
+									</tb-device-filter>
+								</section>
+								<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
+										   ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
+									<md-tooltip md-direction="top">
+										{{ 'device.remove-alias' | translate }}
+									</md-tooltip>
+									<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
+										close
+									</md-icon>
+								</md-button>
+							</div>
+						</div>
+					</div>
+					<div ng-show="!vm.isSingleDeviceAlias && !vm.disableAdd" style="padding-bottom: 10px;">
+						<md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
+							<md-tooltip md-direction="top">
+								{{ 'device.add-alias' | translate }}
+							</md-tooltip>
+							<span translate>action.add</span>
+						</md-button>
+					</div>
+				</fieldset>
+			</div>
+		</md-dialog-content>
+		<md-dialog-actions layout="row">
+			<span flex></span>
+			<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+				{{ 'action.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>
\ No newline at end of file
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 6cb1a07..1c2e461 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -58,7 +58,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
         });
 
         scope.fetchDeviceKeys = function (deviceAliasId, query, type) {
-            var deviceAlias = scope.dashboard.configuration.deviceAliases[deviceAliasId];
+            var deviceAlias = scope.aliasesInfo.deviceAliases[deviceAliasId];
             if (deviceAlias && deviceAlias.deviceId) {
                 return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
             } else {
@@ -69,7 +69,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
         scope.createDeviceAlias = function (event, alias) {
 
             var deferred = $q.defer();
-            var singleDeviceAlias = {id: null, alias: alias, deviceId: null};
+            var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
 
             $mdDialog.show({
                 controller: 'DeviceAliasesController',
@@ -79,7 +79,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
                     config: {
                         deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
                         widgets: null,
-                        isSingleDevice: true,
+                        isSingleDeviceAlias: true,
                         singleDeviceAlias: singleDeviceAlias
                     }
                 },
@@ -89,8 +89,15 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
                 targetEvent: event
             }).then(function (singleDeviceAlias) {
                 scope.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
-                            { alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId };
-                deferred.resolve(singleDeviceAlias);
+                            { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
+                deviceService.processDeviceAliases(scope.dashboard.configuration.deviceAliases).then(
+                    function(resolution) {
+                        if (!resolution.error) {
+                            scope.aliasesInfo = resolution.aliasesInfo;
+                        }
+                        deferred.resolve(singleDeviceAlias);
+                    }
+                );
             }, function () {
                 deferred.reject();
             });
@@ -106,6 +113,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
         link: linker,
         scope: {
             dashboard: '=',
+            aliasesInfo: '=',
             widget: '=',
             theForm: '='
         }
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
index a2cd409..7aa6339 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -20,7 +20,7 @@
 					  ng-model="widgetConfig"
 					  widget-settings-schema="settingsSchema"
 					  datakey-settings-schema="dataKeySettingsSchema"
-					  device-aliases="dashboard.configuration.deviceAliases"
+					  device-aliases="aliasesInfo.deviceAliases"
 					  functions-only="functionsOnly"
 					  fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)"
 					  on-create-device-alias="createDeviceAlias(event, alias)"
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index 6c3492e..3c9f84a 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -24,6 +24,7 @@ import thingsboardApiUser from '../api/user.service';
 import thingsboardApiDashboard from '../api/dashboard.service';
 import thingsboardApiCustomer from '../api/customer.service';
 import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
+import thingsboardDeviceFilter from '../components/device-filter.directive';
 import thingsboardWidgetConfig from '../components/widget-config.directive';
 import thingsboardDashboard from '../components/dashboard.directive';
 import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
@@ -36,12 +37,14 @@ import DashboardRoutes from './dashboard.routes';
 import DashboardsController from './dashboards.controller';
 import DashboardController from './dashboard.controller';
 import DeviceAliasesController from './device-aliases.controller';
+import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
 import DashboardSettingsController from './dashboard-settings.controller';
 import AssignDashboardToCustomerController from './assign-to-customer.controller';
 import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
 import AddWidgetController from './add-widget.controller';
 import DashboardDirective from './dashboard.directive';
 import EditWidgetDirective from './edit-widget.directive';
+import AliasesDeviceSelectDirective from './aliases-device-select.directive';
 
 export default angular.module('thingsboard.dashboard', [
     uiRouter,
@@ -55,6 +58,7 @@ export default angular.module('thingsboard.dashboard', [
     thingsboardApiDashboard,
     thingsboardApiCustomer,
     thingsboardDetailsSidenav,
+    thingsboardDeviceFilter,
     thingsboardWidgetConfig,
     thingsboardDashboard,
     thingsboardExpandFullscreen,
@@ -64,10 +68,12 @@ export default angular.module('thingsboard.dashboard', [
     .controller('DashboardsController', DashboardsController)
     .controller('DashboardController', DashboardController)
     .controller('DeviceAliasesController', DeviceAliasesController)
+    .controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)
     .controller('DashboardSettingsController', DashboardSettingsController)
     .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
     .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
     .controller('AddWidgetController', AddWidgetController)
     .directive('tbDashboardDetails', DashboardDirective)
     .directive('tbEditWidget', EditWidgetDirective)
+    .directive('tbAliasesDeviceSelect', AliasesDeviceSelectDirective)
     .name;
diff --git a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
index a59ba1c..a25f24f 100644
--- a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
+++ b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -45,9 +45,13 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
         };
         aliasesInfo.datasourceAliases[0] = {
             aliasName: deviceName,
-            deviceId: deviceId
+            deviceFilter: {
+                useFilter: false,
+                deviceNameFilter: '',
+                deviceList: [deviceId]
+            }
         };
-        theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, 48, -1, -1);
+        theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, null, 48, -1, -1);
         dashboardService.saveDashboard(theDashboard).then(
             function success(dashboard) {
                 $mdDialog.hide();
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
index afaa785..954242e 100644
--- a/ui/src/app/device/attribute/attribute-table.directive.js
+++ b/ui/src/app/device/attribute/attribute-table.directive.js
@@ -255,8 +255,16 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
             scope.firstBundle = true;
             scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
 
-            scope.deviceAliases = {};
-            scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId};
+            scope.aliasesInfo = {
+                deviceAliases: {
+                    '1': {alias: scope.deviceName, deviceId: scope.deviceId}
+                },
+                deviceAliasesInfo: {
+                    '1': [
+                        {name: scope.deviceName, id: scope.deviceId}
+                    ]
+                }
+            };
 
             var dataKeyType = scope.attributeScope === types.latestTelemetry ?
                 types.dataKeyType.timeseries : types.dataKeyType.attribute;
diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html
index e2efe36..915fbea 100644
--- a/ui/src/app/device/attribute/attribute-table.tpl.html
+++ b/ui/src/app/device/attribute/attribute-table.tpl.html
@@ -156,7 +156,7 @@
             rn-swipe-disabled="true">
             <li ng-repeat="widgets in widgetsList">
                 <tb-dashboard
-                        device-alias-list="deviceAliases"
+                        aliases-info="aliasesInfo"
                         widgets="widgets"
                         get-st-diff="getServerTimeDiff()"
                         columns="20"
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 3f07173..52c05f2 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -41,6 +41,7 @@
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
             <tb-attribute-table flex
                                 device-id="vm.grid.operatingItem().id.id"
+                                device-name="vm.grid.operatingItem().name"
                                 default-attribute-scope="{{vm.types.latestTelemetry.value}}"
                                 disable-attribute-scope-selection="true">
             </tb-attribute-table>
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 2a681a2..b8ba338 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -45,7 +45,25 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         exportToPc(prepareExport(widgetItem), name + '.json');
     }
 
-    function importWidget($event, dashboard) {
+    function prepareDeviceAlias(aliasInfo) {
+        var deviceFilter;
+        if (aliasInfo.deviceId) {
+            deviceFilter = {
+                useFilter: false,
+                deviceNameFilter: '',
+                deviceList: [aliasInfo.deviceId]
+            }
+            delete aliasInfo.deviceId;
+        } else {
+            deviceFilter = aliasInfo.deviceFilter;
+        }
+        return {
+            alias: aliasInfo.aliasName,
+            deviceFilter: deviceFilter
+        };
+    }
+
+    function importWidget($event, dashboard, onAliasesUpdate) {
         openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
             function success(widgetItem) {
                 if (!validateImportedWidget(widgetItem)) {
@@ -66,20 +84,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                         if (datasourceAliases) {
                             for (datasourceIndex in datasourceAliases) {
                                 datasourceAliasesMap[aliasId] = datasourceIndex;
-                                deviceAliases[aliasId] = {
-                                    alias: datasourceAliases[datasourceIndex].aliasName,
-                                    deviceId: datasourceAliases[datasourceIndex].deviceId
-                                };
+                                deviceAliases[aliasId] = prepareDeviceAlias(datasourceAliases[datasourceIndex]);
                                 aliasId++;
                             }
                         }
                         if (targetDeviceAliases) {
                             for (datasourceIndex in targetDeviceAliases) {
                                 targetDeviceAliasesMap[aliasId] = datasourceIndex;
-                                deviceAliases[aliasId] = {
-                                    alias: targetDeviceAliases[datasourceIndex].aliasName,
-                                    deviceId: targetDeviceAliases[datasourceIndex].deviceId
-                                };
+                                deviceAliases[aliasId] = prepareDeviceAlias(targetDeviceAliases[datasourceIndex]);
                                 aliasId++;
                             }
                         }
@@ -97,26 +109,26 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                                                     var datasourceIndex;
                                                     if (datasourceAliasesMap[aliasId]) {
                                                         datasourceIndex = datasourceAliasesMap[aliasId];
-                                                        datasourceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
+                                                        datasourceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
                                                     } else if (targetDeviceAliasesMap[aliasId]) {
                                                         datasourceIndex = targetDeviceAliasesMap[aliasId];
-                                                        targetDeviceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
+                                                        targetDeviceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
                                                     }
                                                 }
-                                                addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                                                addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
                                             },
                                             function fail() {}
                                         );
                                     } else {
-                                        addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                                        addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
                                     }
                                 }
                             );
                         } else {
-                            addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                            addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
                         }
                     } else {
-                        addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                        addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
                     }
                 }
             },
@@ -140,8 +152,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
-    function addImportedWidget(dashboard, widget, aliasesInfo, originalColumns) {
-        itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, -1, -1);
+    function addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns) {
+        itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, -1, -1);
     }
 
     // Dashboard functions
@@ -248,19 +260,18 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
     function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
         var aliasId = aliasIds[index];
         var deviceAlias = deviceAliases[aliasId];
-        if (deviceAlias.deviceId) {
-            deviceService.getDevice(deviceAlias.deviceId, true).then(
-                function success() {
+        deviceService.checkDeviceAlias(deviceAlias).then(
+            function(result) {
+                if (result) {
                     checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
-                },
-                function fail() {
+                } else {
                     var missingDeviceAlias = angular.copy(deviceAlias);
-                    missingDeviceAlias.deviceId = null;
+                    missingDeviceAlias.deviceFilter = null;
                     missingDeviceAliases[aliasId] = missingDeviceAlias;
                     checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
                 }
-            );
-        }
+            }
+        );
     }
 
     function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
@@ -274,7 +285,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                     deviceAliases: missingDeviceAliases,
                     widgets: widgets,
                     isSingleWidget: isSingleWidget,
-                    isSingleDevice: false,
+                    isSingleDeviceAlias: false,
                     singleDeviceAlias: null,
                     customTitle: customTitle,
                     disableAdd: true
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 62ff290..332da30 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -254,14 +254,19 @@ export default angular.module('thingsboard.locale', [])
                     "create-new-dashboard": "Create new dashboard",
                     "dashboard-file": "Dashboard file",
                     "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.",
-                    "dashboard-import-missing-aliases-title": "Select missing devices for dashboard aliases",
+                    "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard",
                     "create-new-widget": "Create new widget",
                     "import-widget": "Import widget",
                     "widget-file": "Widget file",
                     "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.",
-                    "widget-import-missing-aliases-title": "Select missing devices used by widget",
+                    "widget-import-missing-aliases-title": "Configure aliases used by imported widget",
                     "open-toolbar": "Open dashboard toolbar",
-                    "close-toolbar": "Close toolbar"
+                    "close-toolbar": "Close toolbar",
+                    "configuration-error": "Configuration error",
+                    "alias-resolution-error-title": "Dashboard aliases configuration error",
+                    "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.<br/>" +
+                                              "Please contact your administrator in order to resolve this issue.",
+                    "select-devices": "Select devices"
                 },
                 "datakey": {
                     "settings": "Settings",
@@ -301,12 +306,18 @@ export default angular.module('thingsboard.locale', [])
                     "create-new-alias": "Create a new one!",
                     "create-new-key": "Create a new one!",
                     "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.",
-                    "select-device-for-alias": "Select device for '{{alias}}' alias",
+                    "configure-alias": "Configure '{{alias}}' alias",
                     "no-devices-matching": "No devices matching '{{device}}' were found.",
                     "alias": "Alias",
                     "alias-required": "Device alias is required.",
                     "remove-alias": "Remove device alias",
                     "add-alias": "Add device alias",
+                    "name-starts-with": "Name starts with",
+                    "device-list": "Device list",
+                    "use-device-name-filter": "Use filter",
+                    "device-list-empty": "No devices selected.",
+                    "device-name-filter-required": "Device name filter is required.",
+                    "device-name-filter-no-device-matched": "No devices starting with '{{device}}' were found.",
                     "add": "Add Device",
                     "assign-to-customer": "Assign to customer",
                     "assign-device-to-customer": "Assign Device(s) To Customer",
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 2e073b2..0ac71f5 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -43,19 +43,38 @@ function ItemBuffer(bufferStore, types) {
         datasourceAliases: {
             datasourceIndex: {
                 aliasName: "...",
-                deviceId: "..."
+                deviceFilter: "..."
             }
         }
         targetDeviceAliases: {
             targetDeviceAliasIndex: {
                 aliasName: "...",
-                deviceId: "..."
+                deviceFilter: "..."
             }
         }
         ....
      }
     **/
 
+    function getDeviceFilter(alias) {
+        if (alias.deviceId) {
+            return {
+                useFilter: false,
+                deviceNameFilter: '',
+                deviceList: [alias.deviceId]
+            };
+        } else {
+            return alias.deviceFilter;
+        }
+    }
+
+    function prepareAliasInfo(deviceAlias) {
+        return {
+            aliasName: deviceAlias.alias,
+            deviceFilter: getDeviceFilter(deviceAlias)
+        };
+    }
+
     function prepareWidgetItem(dashboard, widget) {
         var aliasesInfo = {
             datasourceAliases: {},
@@ -75,10 +94,7 @@ function ItemBuffer(bufferStore, types) {
                     if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
                         deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
                         if (deviceAlias) {
-                            aliasesInfo.datasourceAliases[i] = {
-                                aliasName: deviceAlias.alias,
-                                deviceId: deviceAlias.deviceId
-                            }
+                            aliasesInfo.datasourceAliases[i] = prepareAliasInfo(deviceAlias);
                         }
                     }
                 }
@@ -89,10 +105,7 @@ function ItemBuffer(bufferStore, types) {
                     if (targetDeviceAliasId) {
                         deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
                         if (deviceAlias) {
-                            aliasesInfo.targetDeviceAliases[i] = {
-                                aliasName: deviceAlias.alias,
-                                deviceId: deviceAlias.deviceId
-                            }
+                            aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(deviceAlias);
                         }
                     }
                 }
@@ -114,7 +127,7 @@ function ItemBuffer(bufferStore, types) {
         return bufferStore.get(WIDGET_ITEM);
     }
 
-    function pasteWidget(targetDashboard, position) {
+    function pasteWidget(targetDashboard, position, onAliasesUpdate) {
         var widgetItemJson = bufferStore.get(WIDGET_ITEM);
         if (widgetItemJson) {
             var widgetItem = angular.fromJson(widgetItemJson);
@@ -127,11 +140,11 @@ function ItemBuffer(bufferStore, types) {
                 targetRow = position.row;
                 targetColumn = position.column;
             }
-            addWidgetToDashboard(targetDashboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
+            addWidgetToDashboard(targetDashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, targetRow, targetColumn);
         }
     }
 
-    function addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, row, column) {
+    function addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, row, column) {
         var theDashboard;
         if (dashboard) {
             theDashboard = dashboard;
@@ -144,7 +157,7 @@ function ItemBuffer(bufferStore, types) {
         if (!theDashboard.configuration.deviceAliases) {
             theDashboard.configuration.deviceAliases = {};
         }
-        updateAliases(theDashboard, widget, aliasesInfo);
+        var newDeviceAliases = updateAliases(theDashboard, widget, aliasesInfo);
 
         if (!theDashboard.configuration.widgets) {
             theDashboard.configuration.widgets = [];
@@ -174,12 +187,19 @@ function ItemBuffer(bufferStore, types) {
             widget.row = row;
             widget.col = 0;
         }
+        var aliasesUpdated = !angular.equals(newDeviceAliases, theDashboard.configuration.deviceAliases);
+        if (aliasesUpdated) {
+            theDashboard.configuration.deviceAliases = newDeviceAliases;
+            if (onAliasesUpdate) {
+                onAliasesUpdate();
+            }
+        }
         theDashboard.configuration.widgets.push(widget);
         return theDashboard;
     }
 
     function updateAliases(dashboard, widget, aliasesInfo) {
-        var deviceAliases = dashboard.configuration.deviceAliases;
+        var deviceAliases = angular.copy(dashboard.configuration.deviceAliases);
         var aliasInfo;
         var newAliasId;
         for (var datasourceIndex in aliasesInfo.datasourceAliases) {
@@ -192,12 +212,19 @@ function ItemBuffer(bufferStore, types) {
             newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
             widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
         }
+        return deviceAliases;
+    }
+
+    function isDeviceFiltersEqual(alias1, alias2) {
+        var filter1 = getDeviceFilter(alias1);
+        var filter2 = getDeviceFilter(alias2);
+        return angular.equals(filter1, filter2);
     }
 
     function getDeviceAliasId(deviceAliases, aliasInfo) {
         var newAliasId;
         for (var aliasId in deviceAliases) {
-            if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) {
+            if (isDeviceFiltersEqual(deviceAliases[aliasId], aliasInfo)) {
                 newAliasId = aliasId;
                 break;
             }
@@ -209,7 +236,7 @@ function ItemBuffer(bufferStore, types) {
                 newAliasId = Math.max(newAliasId, aliasId);
             }
             newAliasId++;
-            deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId};
+            deviceAliases[newAliasId] = {alias: newAliasName, deviceFilter: aliasInfo.deviceFilter};
         }
         return newAliasId;
     }
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js
index 876f0d4..6f8530f 100644
--- a/ui/src/app/widget/lib/flot-widget.js
+++ b/ui/src/app/widget/lib/flot-widget.js
@@ -35,7 +35,6 @@ export default class TbFlot {
         var colors = [];
         for (var i in ctx.data) {
             var series = ctx.data[i];
-            series.label = series.dataKey.label;
             colors.push(series.dataKey.color);
             var keySettings = series.dataKey.settings;
 
@@ -130,7 +129,7 @@ export default class TbFlot {
 
         if (this.chartType === 'pie') {
             ctx.tooltipFormatter = function(item) {
-                var divElement = seriesInfoDiv(item.series.label, item.series.color,
+                var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
                     item.datapoint[1][0][1], tbFlot.ctx.trackUnits, tbFlot.ctx.trackDecimals, true, item.series.percent);
                 return divElement.prop('outerHTML');
             };
@@ -313,7 +312,7 @@ export default class TbFlot {
 
             if (options.series.pie.label.show) {
                 options.series.pie.label.formatter = function (label, series) {
-                    return "<div class='pie-label'>" + label + "<br/>" + Math.round(series.percent) + "%</div>";
+                    return "<div class='pie-label'>" + series.dataKey.label + "<br/>" + Math.round(series.percent) + "%</div>";
                 }
                 options.series.pie.label.radius = 3/4;
                 options.series.pie.label.background = {
@@ -346,7 +345,7 @@ export default class TbFlot {
     }
 
     update() {
-        if (!this.isMouseInteraction) {
+        if (!this.isMouseInteraction && this.ctx.plot) {
             if (this.chartType === 'line' || this.chartType === 'bar') {
                 this.options.xaxis.min = this.ctx.timeWindow.minTime;
                 this.options.xaxis.max = this.ctx.timeWindow.maxTime;
@@ -918,7 +917,7 @@ export default class TbFlot {
                     value: value,
                     hoverIndex: hoverIndex,
                     color: series.dataKey.color,
-                    label: series.label,
+                    label: series.dataKey.label,
                     time: pointTime,
                     distance: hoverDistance,
                     index: i