Commit 207642e8c3ad7c845d7bbd1fb20d585031ee3750

Authored by Igor Kulikov
1 parent 6c33dc92

UI: Ability to define device filters for dashboard aliases

Showing 44 changed files with 1445 additions and 318 deletions
... ... @@ -157,6 +157,16 @@ public abstract class BaseController {
157 157 }
158 158 }
159 159
  160 + void checkArrayParameter(String name, String[] params) throws ThingsboardException {
  161 + if (params == null || params.length == 0) {
  162 + throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
  163 + } else {
  164 + for (String param : params) {
  165 + checkParameter(name, param);
  166 + }
  167 + }
  168 + }
  169 +
160 170 UUID toUUID(String id) {
161 171 return UUID.fromString(id);
162 172 }
... ...
... ... @@ -15,6 +15,7 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import com.google.common.util.concurrent.ListenableFuture;
18 19 import org.springframework.http.HttpStatus;
19 20 import org.springframework.security.access.prepost.PreAuthorize;
20 21 import org.springframework.web.bind.annotation.*;
... ... @@ -22,6 +23,7 @@ import org.thingsboard.server.common.data.Device;
22 23 import org.thingsboard.server.common.data.id.CustomerId;
23 24 import org.thingsboard.server.common.data.id.DeviceId;
24 25 import org.thingsboard.server.common.data.id.TenantId;
  26 +import org.thingsboard.server.common.data.id.UUIDBased;
25 27 import org.thingsboard.server.common.data.page.TextPageData;
26 28 import org.thingsboard.server.common.data.page.TextPageLink;
27 29 import org.thingsboard.server.common.data.security.DeviceCredentials;
... ... @@ -29,6 +31,11 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
29 31 import org.thingsboard.server.dao.model.ModelConstants;
30 32 import org.thingsboard.server.exception.ThingsboardException;
31 33 import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
  34 +import org.thingsboard.server.service.security.model.SecurityUser;
  35 +
  36 +import java.util.ArrayList;
  37 +import java.util.List;
  38 +import java.util.UUID;
32 39
33 40 @RestController
34 41 @RequestMapping("/api")
... ... @@ -189,4 +196,30 @@ public class DeviceController extends BaseController {
189 196 throw handleException(e);
190 197 }
191 198 }
  199 +
  200 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
  201 + @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
  202 + @ResponseBody
  203 + public List<Device> getDevicesByIds(
  204 + @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException {
  205 + checkArrayParameter("deviceIds", strDeviceIds);
  206 + try {
  207 + SecurityUser user = getCurrentUser();
  208 + TenantId tenantId = user.getTenantId();
  209 + CustomerId customerId = user.getCustomerId();
  210 + List<DeviceId> deviceIds = new ArrayList<>();
  211 + for (String strDeviceId : strDeviceIds) {
  212 + deviceIds.add(new DeviceId(toUUID(strDeviceId)));
  213 + }
  214 + ListenableFuture<List<Device>> devices;
  215 + if (customerId == null || customerId.isNullUid()) {
  216 + devices = deviceService.findDevicesByTenantIdAndIdsAsync(tenantId, deviceIds);
  217 + } else {
  218 + devices = deviceService.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, deviceIds);
  219 + }
  220 + return checkNotNull(devices.get());
  221 + } catch (Exception e) {
  222 + throw handleException(e);
  223 + }
  224 + }
192 225 }
... ...
... ... @@ -64,6 +64,27 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
64 64 return list;
65 65 }
66 66
  67 + protected ListenableFuture<List<T>> findListByStatementAsync(Statement statement) {
  68 + if (statement != null) {
  69 + statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
  70 + ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
  71 + ListenableFuture<List<T>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<T>>() {
  72 + @Nullable
  73 + @Override
  74 + public List<T> apply(@Nullable ResultSet resultSet) {
  75 + Result<T> result = getMapper().map(resultSet);
  76 + if (result != null) {
  77 + return result.all();
  78 + } else {
  79 + return Collections.emptyList();
  80 + }
  81 + }
  82 + });
  83 + return result;
  84 + }
  85 + return Futures.immediateFuture(Collections.emptyList());
  86 + }
  87 +
67 88 protected T findOneByStatement(Statement statement) {
68 89 T object = null;
69 90 if (statement != null) {
... ...
... ... @@ -56,4 +56,12 @@ public abstract class DaoUtil {
56 56 return id;
57 57 }
58 58
  59 + public static List<UUID> toUUIDs(List<? extends UUIDBased> idBasedIds) {
  60 + List<UUID> ids = new ArrayList<>();
  61 + for (UUIDBased idBased : idBasedIds) {
  62 + ids.add(getId(idBased));
  63 + }
  64 + return ids;
  65 + }
  66 +
59 67 }
... ...
... ... @@ -19,6 +19,7 @@ import java.util.List;
19 19 import java.util.Optional;
20 20 import java.util.UUID;
21 21
  22 +import com.google.common.util.concurrent.ListenableFuture;
22 23 import org.thingsboard.server.common.data.Device;
23 24 import org.thingsboard.server.common.data.page.TextPageLink;
24 25 import org.thingsboard.server.dao.Dao;
... ... @@ -46,7 +47,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
46 47 * @return the list of device objects
47 48 */
48 49 List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink);
49   -
  50 +
  51 + /**
  52 + * Find devices by tenantId and devices Ids.
  53 + *
  54 + * @param tenantId the tenantId
  55 + * @param deviceIds the device Ids
  56 + * @return the list of device objects
  57 + */
  58 + ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds);
  59 +
50 60 /**
51 61 * Find devices by tenantId, customerId and page link.
52 62 *
... ... @@ -58,6 +68,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
58 68 List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
59 69
60 70 /**
  71 + * Find devices by tenantId, customerId and devices Ids.
  72 + *
  73 + * @param tenantId the tenantId
  74 + * @param customerId the customerId
  75 + * @param deviceIds the device Ids
  76 + * @return the list of device objects
  77 + */
  78 + ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds);
  79 +
  80 + /**
61 81 * Find devices by tenantId and device name.
62 82 *
63 83 * @param tenantId the tenantId
... ...
... ... @@ -16,12 +16,14 @@
16 16 package org.thingsboard.server.dao.device;
17 17
18 18 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
  19 +import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
19 20 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
20 21 import static org.thingsboard.server.dao.model.ModelConstants.*;
21 22
22 23 import java.util.*;
23 24
24 25 import com.datastax.driver.core.querybuilder.Select;
  26 +import com.google.common.util.concurrent.ListenableFuture;
25 27 import lombok.extern.slf4j.Slf4j;
26 28 import org.springframework.stereotype.Component;
27 29 import org.thingsboard.server.common.data.Device;
... ... @@ -62,6 +64,16 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
62 64 }
63 65
64 66 @Override
  67 + public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
  68 + log.debug("Try to find devices by tenantId [{}] and device Ids [{}]", tenantId, deviceIds);
  69 + Select select = select().from(getColumnFamilyName());
  70 + Select.Where query = select.where();
  71 + query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
  72 + query.and(in(ID_PROPERTY, deviceIds));
  73 + return findListByStatementAsync(query);
  74 + }
  75 +
  76 + @Override
65 77 public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
66 78 log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
67 79 List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
... ... @@ -74,6 +86,17 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
74 86 }
75 87
76 88 @Override
  89 + public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
  90 + log.debug("Try to find devices by tenantId [{}], customerId [{}] and device Ids [{}]", tenantId, customerId, deviceIds);
  91 + Select select = select().from(getColumnFamilyName());
  92 + Select.Where query = select.where();
  93 + query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
  94 + query.and(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId));
  95 + query.and(in(ID_PROPERTY, deviceIds));
  96 + return findListByStatementAsync(query);
  97 + }
  98 +
  99 + @Override
77 100 public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) {
78 101 Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME);
79 102 Select.Where query = select.where();
... ...
... ... @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
23 23 import org.thingsboard.server.common.data.page.TextPageData;
24 24 import org.thingsboard.server.common.data.page.TextPageLink;
25 25
  26 +import java.util.List;
26 27 import java.util.Optional;
27 28
28 29 public interface DeviceService {
... ... @@ -43,9 +44,13 @@ public interface DeviceService {
43 44
44 45 TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
45 46
  47 + ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
  48 +
46 49 void deleteDevicesByTenantId(TenantId tenantId);
47 50
48 51 TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
49 52
  53 + ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
  54 +
50 55 void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
51 56 }
... ...
... ... @@ -45,8 +45,10 @@ import java.util.Optional;
45 45
46 46 import static org.thingsboard.server.dao.DaoUtil.convertDataList;
47 47 import static org.thingsboard.server.dao.DaoUtil.getData;
  48 +import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
48 49 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
49 50 import static org.thingsboard.server.dao.service.Validator.validateId;
  51 +import static org.thingsboard.server.dao.service.Validator.validateIds;
50 52 import static org.thingsboard.server.dao.service.Validator.validatePageLink;
51 53
52 54 @Service
... ... @@ -144,6 +146,16 @@ public class DeviceServiceImpl implements DeviceService {
144 146 }
145 147
146 148 @Override
  149 + public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
  150 + log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
  151 + validateId(tenantId, "Incorrect tenantId " + tenantId);
  152 + validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
  153 + ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceIds));
  154 + return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
  155 + }
  156 +
  157 +
  158 + @Override
147 159 public void deleteDevicesByTenantId(TenantId tenantId) {
148 160 log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
149 161 validateId(tenantId, "Incorrect tenantId " + tenantId);
... ... @@ -162,6 +174,17 @@ public class DeviceServiceImpl implements DeviceService {
162 174 }
163 175
164 176 @Override
  177 + public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
  178 + log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);
  179 + validateId(tenantId, "Incorrect tenantId " + tenantId);
  180 + validateId(customerId, "Incorrect customerId " + customerId);
  181 + validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
  182 + ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(),
  183 + customerId.getId(), toUUIDs(deviceIds));
  184 + return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
  185 + }
  186 +
  187 + @Override
165 188 public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) {
166 189 log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId);
167 190 validateId(tenantId, "Incorrect tenantId " + tenantId);
... ...
... ... @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.UUIDBased;
19 19 import org.thingsboard.server.common.data.page.TextPageLink;
20 20 import org.thingsboard.server.dao.exception.IncorrectParameterException;
21 21
  22 +import java.util.List;
22 23 import java.util.UUID;
23 24
24 25 public class Validator {
... ... @@ -78,6 +79,23 @@ public class Validator {
78 79 }
79 80
80 81 /**
  82 + * This method validate list of <code>UUIDBased</code> ids. If at least one of the ids is null than throw
  83 + * <code>IncorrectParameterException</code> exception
  84 + *
  85 + * @param ids the list of ids
  86 + * @param errorMessage the error message for exception
  87 + */
  88 + public static void validateIds(List<? extends UUIDBased> ids, String errorMessage) {
  89 + if (ids == null || ids.isEmpty()) {
  90 + throw new IncorrectParameterException(errorMessage);
  91 + } else {
  92 + for (UUIDBased id : ids) {
  93 + validateId(id, errorMessage);
  94 + }
  95 + }
  96 + }
  97 +
  98 + /**
81 99 * This method validate <code>PageLink</code> page link. If pageLink is invalid than throw
82 100 * <code>IncorrectParameterException</code> exception
83 101 *
... ...
... ... @@ -188,7 +188,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges',
188 188
189 189 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
190 190 VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table',
191   -'{"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}"}',
  191 +'{"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}"}',
192 192 'Timeseries table' );
193 193
194 194 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
... ...
... ... @@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
20 20 .name;
21 21
22 22 /*@ngInject*/
23   -function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
  23 +function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
24 24
25 25
26 26 var deviceAttributesSubscriptionMap = {};
... ... @@ -30,6 +30,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
30 30 deleteDevice: deleteDevice,
31 31 getCustomerDevices: getCustomerDevices,
32 32 getDevice: getDevice,
  33 + getDevices: getDevices,
  34 + processDeviceAliases: processDeviceAliases,
  35 + checkDeviceAlias: checkDeviceAlias,
33 36 getDeviceCredentials: getDeviceCredentials,
34 37 getDeviceKeys: getDeviceKeys,
35 38 getDeviceTimeseriesValues: getDeviceTimeseriesValues,
... ... @@ -99,6 +102,200 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
99 102 return deferred.promise;
100 103 }
101 104
  105 + function getDevices(deviceIds) {
  106 + var deferred = $q.defer();
  107 + var ids = '';
  108 + for (var i=0;i<deviceIds.length;i++) {
  109 + if (i>0) {
  110 + ids += ',';
  111 + }
  112 + ids += deviceIds[i];
  113 + }
  114 + var url = '/api/devices?deviceIds=' + ids;
  115 + $http.get(url, null).then(function success(response) {
  116 + var devices = response.data;
  117 + devices.sort(function (device1, device2) {
  118 + var id1 = device1.id.id;
  119 + var id2 = device2.id.id;
  120 + var index1 = deviceIds.indexOf(id1);
  121 + var index2 = deviceIds.indexOf(id2);
  122 + return index1 - index2;
  123 + });
  124 + deferred.resolve(devices);
  125 + }, function fail(response) {
  126 + deferred.reject(response.data);
  127 + });
  128 + return deferred.promise;
  129 + }
  130 +
  131 + function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
  132 + var deferred = $q.defer();
  133 + var user = userService.getCurrentUser();
  134 + var promise;
  135 + var pageLink = {limit: limit, textSearch: deviceNameFilter};
  136 + if (user.authority === 'CUSTOMER_USER') {
  137 + var customerId = user.customerId;
  138 + promise = getCustomerDevices(customerId, pageLink);
  139 + } else {
  140 + promise = getTenantDevices(pageLink);
  141 + }
  142 + promise.then(
  143 + function success(result) {
  144 + if (result.data && result.data.length > 0) {
  145 + deferred.resolve(result.data);
  146 + } else {
  147 + deferred.resolve(null);
  148 + }
  149 + },
  150 + function fail() {
  151 + deferred.resolve(null);
  152 + }
  153 + );
  154 + return deferred.promise;
  155 + }
  156 +
  157 + function deviceToDeviceInfo(device) {
  158 + return { name: device.name, id: device.id.id };
  159 + }
  160 +
  161 + function devicesToDevicesInfo(devices) {
  162 + var devicesInfo = [];
  163 + for (var d in devices) {
  164 + devicesInfo.push(deviceToDeviceInfo(devices[d]));
  165 + }
  166 + return devicesInfo;
  167 + }
  168 +
  169 + function processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred) {
  170 + if (index < aliasIds.length) {
  171 + var aliasId = aliasIds[index];
  172 + var deviceAlias = deviceAliases[aliasId];
  173 + var alias = deviceAlias.alias;
  174 + if (!deviceAlias.deviceFilter) {
  175 + getDevice(deviceAlias.deviceId).then(
  176 + function success(device) {
  177 + var resolvedAlias = {alias: alias, deviceId: device.id.id};
  178 + resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
  179 + resolution.aliasesInfo.deviceAliasesInfo[aliasId] = [
  180 + deviceToDeviceInfo(device)
  181 + ];
  182 + index++;
  183 + processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
  184 + },
  185 + function fail() {
  186 + if (!resolution.error) {
  187 + resolution.error = 'dashboard.invalid-aliases-config';
  188 + }
  189 + index++;
  190 + processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
  191 + }
  192 + );
  193 + } else {
  194 + var deviceFilter = deviceAlias.deviceFilter;
  195 + if (deviceFilter.useFilter) {
  196 + var deviceNameFilter = deviceFilter.deviceNameFilter;
  197 + fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
  198 + function(devices) {
  199 + if (devices && devices != null) {
  200 + var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
  201 + resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
  202 + resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
  203 + index++;
  204 + processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
  205 + } else {
  206 + if (!resolution.error) {
  207 + resolution.error = 'dashboard.invalid-aliases-config';
  208 + }
  209 + index++;
  210 + processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
  211 + }
  212 + });
  213 + } else {
  214 + var deviceList = deviceFilter.deviceList;
  215 + getDevices(deviceList).then(
  216 + function success(devices) {
  217 + if (devices && devices.length > 0) {
  218 + var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
  219 + resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
  220 + resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
  221 + index++;
  222 + processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
  223 + } else {
  224 + if (!resolution.error) {
  225 + resolution.error = 'dashboard.invalid-aliases-config';
  226 + }
  227 + index++;
  228 + processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
  229 + }
  230 + },
  231 + function fail() {
  232 + if (!resolution.error) {
  233 + resolution.error = 'dashboard.invalid-aliases-config';
  234 + }
  235 + index++;
  236 + processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
  237 + }
  238 + );
  239 + }
  240 + }
  241 + } else {
  242 + deferred.resolve(resolution);
  243 + }
  244 + }
  245 +
  246 + function processDeviceAliases(deviceAliases) {
  247 + var deferred = $q.defer();
  248 + var resolution = {
  249 + aliasesInfo: {
  250 + deviceAliases: {},
  251 + deviceAliasesInfo: {}
  252 + }
  253 + };
  254 + var aliasIds = [];
  255 + if (deviceAliases) {
  256 + for (var aliasId in deviceAliases) {
  257 + aliasIds.push(aliasId);
  258 + }
  259 + }
  260 + processDeviceAlias(0, aliasIds, deviceAliases, resolution, deferred);
  261 + return deferred.promise;
  262 + }
  263 +
  264 + function checkDeviceAlias(deviceAlias) {
  265 + var deferred = $q.defer();
  266 + var deviceFilter;
  267 + if (deviceAlias.deviceId) {
  268 + deviceFilter = {
  269 + useFilter: false,
  270 + deviceNameFilter: '',
  271 + deviceList: [deviceAlias.deviceId]
  272 + }
  273 + } else {
  274 + deviceFilter = deviceAlias.deviceFilter;
  275 + }
  276 + var promise;
  277 + if (deviceFilter.useFilter) {
  278 + var deviceNameFilter = deviceFilter.deviceNameFilter;
  279 + promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
  280 + } else {
  281 + var deviceList = deviceFilter.deviceList;
  282 + promise = getDevices(deviceList);
  283 + }
  284 + promise.then(
  285 + function success(devices) {
  286 + if (devices && devices.length > 0) {
  287 + deferred.resolve(true);
  288 + } else {
  289 + deferred.resolve(false);
  290 + }
  291 + },
  292 + function fail() {
  293 + deferred.resolve(false);
  294 + }
  295 + );
  296 + return deferred.promise;
  297 + }
  298 +
102 299 function saveDevice(device) {
103 300 var deferred = $q.defer();
104 301 var url = '/api/device';
... ...
... ... @@ -51,7 +51,7 @@ function Dashboard() {
51 51 scope: true,
52 52 bindToController: {
53 53 widgets: '=',
54   - deviceAliasList: '=',
  54 + aliasesInfo: '=',
55 55 dashboardTimewindow: '=?',
56 56 columns: '=',
57 57 margins: '=',
... ... @@ -274,8 +274,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
274 274 $scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
275 275 });
276 276
277   - $scope.$watch('vm.deviceAliasList', function () {
278   - $scope.$broadcast('deviceAliasListChanged', vm.deviceAliasList);
  277 + $scope.$watch('vm.aliasesInfo.deviceAliases', function () {
  278 + $scope.$broadcast('deviceAliasListChanged', vm.aliasesInfo);
279 279 }, true);
280 280
281 281 $scope.$on('gridster-resized', function (event, sizes, theGridster) {
... ...
... ... @@ -90,7 +90,7 @@
90 90 <div flex tb-widget
91 91 locals="{ visibleRect: vm.visibleRect,
92 92 widget: widget,
93   - deviceAliasList: vm.deviceAliasList,
  93 + aliasesInfo: vm.aliasesInfo,
94 94 isEdit: vm.isEdit,
95 95 stDiff: vm.stDiff,
96 96 dashboardTimewindow: vm.dashboardTimewindow,
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import deviceFilterTemplate from './device-filter.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +import './device-filter.scss';
  24 +
  25 +export default angular.module('thingsboard.directives.deviceFilter', [])
  26 + .directive('tbDeviceFilter', DeviceFilter)
  27 + .name;
  28 +
  29 +/*@ngInject*/
  30 +function DeviceFilter($compile, $templateCache, $q, deviceService) {
  31 +
  32 + var linker = function (scope, element, attrs, ngModelCtrl) {
  33 +
  34 + var template = $templateCache.get(deviceFilterTemplate);
  35 + element.html(template);
  36 +
  37 + scope.ngModelCtrl = ngModelCtrl;
  38 +
  39 + scope.fetchDevices = function(searchText, limit) {
  40 + var pageLink = {limit: limit, textSearch: searchText};
  41 +
  42 + var deferred = $q.defer();
  43 +
  44 + deviceService.getTenantDevices(pageLink).then(function success(result) {
  45 + deferred.resolve(result.data);
  46 + }, function fail() {
  47 + deferred.reject();
  48 + });
  49 +
  50 + return deferred.promise;
  51 + }
  52 +
  53 + scope.updateValidity = function() {
  54 + if (ngModelCtrl.$viewValue) {
  55 + var value = ngModelCtrl.$viewValue;
  56 + var valid;
  57 + if (value.useFilter) {
  58 + ngModelCtrl.$setValidity('deviceList', true);
  59 + if (angular.isDefined(value.deviceNameFilter) && value.deviceNameFilter.length > 0) {
  60 + ngModelCtrl.$setValidity('deviceNameFilter', true);
  61 + valid = angular.isDefined(scope.model.matchingFilterDevice) && scope.model.matchingFilterDevice != null;
  62 + ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', valid);
  63 + } else {
  64 + ngModelCtrl.$setValidity('deviceNameFilter', false);
  65 + }
  66 + } else {
  67 + ngModelCtrl.$setValidity('deviceNameFilter', true);
  68 + ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', true);
  69 + valid = angular.isDefined(value.deviceList) && value.deviceList.length > 0;
  70 + ngModelCtrl.$setValidity('deviceList', valid);
  71 + }
  72 + }
  73 + }
  74 +
  75 + ngModelCtrl.$render = function () {
  76 + destroyWatchers();
  77 + scope.model = {
  78 + useFilter: false,
  79 + deviceList: [],
  80 + deviceNameFilter: ''
  81 + }
  82 + if (ngModelCtrl.$viewValue) {
  83 + var value = ngModelCtrl.$viewValue;
  84 + var model = scope.model;
  85 + model.useFilter = value.useFilter === true ? true: false;
  86 + model.deviceList = [];
  87 + model.deviceNameFilter = value.deviceNameFilter || '';
  88 + processDeviceNameFilter(model.deviceNameFilter).then(
  89 + function(device) {
  90 + scope.model.matchingFilterDevice = device;
  91 + if (value.deviceList && value.deviceList.length > 0) {
  92 + deviceService.getDevices(value.deviceList).then(function (devices) {
  93 + model.deviceList = devices;
  94 + updateMatchingDevice();
  95 + initWatchers();
  96 + });
  97 + } else {
  98 + updateMatchingDevice();
  99 + initWatchers();
  100 + }
  101 + }
  102 + )
  103 + }
  104 + }
  105 +
  106 + function updateMatchingDevice() {
  107 + if (scope.model.useFilter) {
  108 + scope.model.matchingDevice = scope.model.matchingFilterDevice;
  109 + } else {
  110 + if (scope.model.deviceList && scope.model.deviceList.length > 0) {
  111 + scope.model.matchingDevice = scope.model.deviceList[0];
  112 + } else {
  113 + scope.model.matchingDevice = null;
  114 + }
  115 + }
  116 + }
  117 +
  118 + function processDeviceNameFilter(deviceNameFilter) {
  119 + var deferred = $q.defer();
  120 + if (angular.isDefined(deviceNameFilter) && deviceNameFilter.length > 0) {
  121 + scope.fetchDevices(deviceNameFilter, 1).then(function (devices) {
  122 + if (devices && devices.length > 0) {
  123 + deferred.resolve(devices[0]);
  124 + } else {
  125 + deferred.resolve(null);
  126 + }
  127 + });
  128 + } else {
  129 + deferred.resolve(null);
  130 + }
  131 + return deferred.promise;
  132 + }
  133 +
  134 + function destroyWatchers() {
  135 + if (scope.deviceListDeregistration) {
  136 + scope.deviceListDeregistration();
  137 + scope.deviceListDeregistration = null;
  138 + }
  139 + if (scope.useFilterDeregistration) {
  140 + scope.useFilterDeregistration();
  141 + scope.useFilterDeregistration = null;
  142 + }
  143 + if (scope.deviceNameFilterDeregistration) {
  144 + scope.deviceNameFilterDeregistration();
  145 + scope.deviceNameFilterDeregistration = null;
  146 + }
  147 + if (scope.matchingDeviceDeregistration) {
  148 + scope.matchingDeviceDeregistration();
  149 + scope.matchingDeviceDeregistration = null;
  150 + }
  151 + }
  152 +
  153 + function initWatchers() {
  154 + scope.deviceListDeregistration = scope.$watch('model.deviceList', function () {
  155 + if (ngModelCtrl.$viewValue) {
  156 + var value = ngModelCtrl.$viewValue;
  157 + value.deviceList = [];
  158 + if (scope.model.deviceList && scope.model.deviceList.length > 0) {
  159 + for (var i in scope.model.deviceList) {
  160 + value.deviceList.push(scope.model.deviceList[i].id.id);
  161 + }
  162 + }
  163 + updateMatchingDevice();
  164 + ngModelCtrl.$setViewValue(value);
  165 + scope.updateValidity();
  166 + }
  167 + }, true);
  168 + scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
  169 + if (ngModelCtrl.$viewValue) {
  170 + var value = ngModelCtrl.$viewValue;
  171 + value.useFilter = scope.model.useFilter;
  172 + updateMatchingDevice();
  173 + ngModelCtrl.$setViewValue(value);
  174 + scope.updateValidity();
  175 + }
  176 + });
  177 + scope.deviceNameFilterDeregistration = scope.$watch('model.deviceNameFilter', function (newNameFilter, prevNameFilter) {
  178 + if (ngModelCtrl.$viewValue) {
  179 + if (!angular.equals(newNameFilter, prevNameFilter)) {
  180 + var value = ngModelCtrl.$viewValue;
  181 + value.deviceNameFilter = scope.model.deviceNameFilter;
  182 + processDeviceNameFilter(value.deviceNameFilter).then(
  183 + function(device) {
  184 + scope.model.matchingFilterDevice = device;
  185 + updateMatchingDevice();
  186 + ngModelCtrl.$setViewValue(value);
  187 + scope.updateValidity();
  188 + }
  189 + );
  190 + }
  191 + }
  192 + });
  193 +
  194 + scope.matchingDeviceDeregistration = scope.$watch('model.matchingDevice', function (newMatchingDevice, prevMatchingDevice) {
  195 + if (!angular.equals(newMatchingDevice, prevMatchingDevice)) {
  196 + if (scope.onMatchingDeviceChange) {
  197 + scope.onMatchingDeviceChange({device: newMatchingDevice});
  198 + }
  199 + }
  200 + });
  201 + }
  202 +
  203 + $compile(element.contents())(scope);
  204 +
  205 + }
  206 +
  207 + return {
  208 + restrict: "E",
  209 + require: "^ngModel",
  210 + link: linker,
  211 + scope: {
  212 + isEdit: '=',
  213 + onMatchingDeviceChange: '&'
  214 + }
  215 + };
  216 +
  217 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +.tb-device-filter {
  17 + #device_list_chips {
  18 + .md-chips {
  19 + padding-bottom: 1px;
  20 + }
  21 + }
  22 + .device-name-filter-input {
  23 + margin-top: 10px;
  24 + margin-bottom: 0px;
  25 + .md-errors-spacer {
  26 + min-height: 0px;
  27 + }
  28 + }
  29 + .tb-filter-switch {
  30 + padding-left: 10px;
  31 + .filter-switch {
  32 + margin: 0;
  33 + }
  34 + .filter-label {
  35 + margin: 5px 0;
  36 + }
  37 + }
  38 + .tb-error-messages {
  39 + margin-top: -11px;
  40 + height: 35px;
  41 + .tb-error-message {
  42 + padding-left: 1px;
  43 + }
  44 + }
  45 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section layout='column' class="tb-device-filter">
  19 + <section layout='row'>
  20 + <section layout="column" flex ng-show="!model.useFilter">
  21 + <md-chips flex
  22 + id="device_list_chips"
  23 + ng-required="!useFilter"
  24 + ng-model="model.deviceList" md-autocomplete-snap
  25 + md-require-match="true">
  26 + <md-autocomplete
  27 + md-no-cache="true"
  28 + id="device"
  29 + md-selected-item="selectedDevice"
  30 + md-search-text="deviceSearchText"
  31 + md-items="item in fetchDevices(deviceSearchText, 10)"
  32 + md-item-text="item.name"
  33 + md-min-length="0"
  34 + placeholder="{{ 'device.device-list' | translate }}">
  35 + <md-item-template>
  36 + <span md-highlight-text="deviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
  37 + </md-item-template>
  38 + <md-not-found>
  39 + <span translate translate-values='{ device: deviceSearchText }'>device.no-devices-matching</span>
  40 + </md-not-found>
  41 + </md-autocomplete>
  42 + <md-chip-template>
  43 + <span>
  44 + <strong>{{$chip.name}}</strong>
  45 + </span>
  46 + </md-chip-template>
  47 + </md-chips>
  48 + </section>
  49 + <section layout="row" flex ng-show="model.useFilter">
  50 + <md-input-container flex class="device-name-filter-input">
  51 + <label translate>device.name-starts-with</label>
  52 + <input ng-model="model.deviceNameFilter" aria-label="{{ 'device.name-starts-with' | translate }}">
  53 + </md-input-container>
  54 + </section>
  55 + <section class="tb-filter-switch" layout="column" layout-align="center center">
  56 + <label class="tb-small filter-label" translate>device.use-device-name-filter</label>
  57 + <md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
  58 + </md-switch>
  59 + </section>
  60 + </section>
  61 + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
  62 + <div translate ng-message="deviceList" class="tb-error-message">device.device-list-empty</div>
  63 + <div translate ng-message="deviceNameFilter" class="tb-error-message">device.device-name-filter-required</div>
  64 + <div translate translate-values='{ device: model.deviceNameFilter }' ng-message="deviceNameFilterDeviceMatch"
  65 + class="tb-error-message">device.device-name-filter-no-device-matched</div>
  66 + </div>
  67 +</section>
\ No newline at end of file
... ...
... ... @@ -44,8 +44,10 @@ function Legend($compile, $templateCache, types) {
44 44 scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
45 45 scope.legendConfig.position === types.position.top.value;
46 46
47   - scope.$on('legendDataUpdated', function () {
48   - scope.$digest();
  47 + scope.$on('legendDataUpdated', function (event, apply) {
  48 + if (apply) {
  49 + scope.$digest();
  50 + }
49 51 });
50 52
51 53 scope.toggleHideData = function(index) {
... ... @@ -62,15 +64,14 @@ function Legend($compile, $templateCache, types) {
62 64 data: []
63 65
64 66 key: {
65   - label: '',
66   - color: ''
67   - dataIndex: 0
  67 + dataKey: dataKey,
  68 + dataIndex: 0
68 69 }
69 70 data: {
70   - min: null,
71   - max: null,
72   - avg: null,
73   - total: null
  71 + min: null,
  72 + max: null,
  73 + avg: null,
  74 + total: null
74 75 }
75 76 };*/
76 77
... ...
... ... @@ -27,11 +27,11 @@
27 27 </thead>
28 28 <tbody>
29 29 <tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys">
30   - <td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.color}"></span></td>
  30 + <td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.dataKey.color}"></span></td>
31 31 <td class="tb-legend-label"
32 32 ng-click="toggleHideData(legendKey.dataIndex)"
33 33 ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }">
34   - {{ legendKey.label }}
  34 + {{ legendKey.dataKey.label }}
35 35 </td>
36 36 <td class="tb-legend-value" ng-if="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td>
37 37 <td class="tb-legend-value" ng-if="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td>
... ...
... ... @@ -84,6 +84,13 @@ function Timeinterval($compile, $templateCache, timeService) {
84 84 scope.rendered = true;
85 85 }
86 86
  87 + function calculateIntervalMs() {
  88 + return (scope.days * 86400 +
  89 + scope.hours * 3600 +
  90 + scope.mins * 60 +
  91 + scope.secs) * 1000;
  92 + }
  93 +
87 94 scope.updateView = function () {
88 95 if (!scope.rendered) {
89 96 return;
... ... @@ -92,11 +99,11 @@ function Timeinterval($compile, $templateCache, timeService) {
92 99 var intervalMs;
93 100 if (!scope.advanced) {
94 101 intervalMs = scope.intervalMs;
  102 + if (!intervalMs || isNaN(intervalMs)) {
  103 + intervalMs = calculateIntervalMs();
  104 + }
95 105 } else {
96   - intervalMs = (scope.days * 86400 +
97   - scope.hours * 3600 +
98   - scope.mins * 60 +
99   - scope.secs) * 1000;
  106 + intervalMs = calculateIntervalMs();
100 107 }
101 108 if (!isNaN(intervalMs) && intervalMs > 0) {
102 109 value = intervalMs;
... ... @@ -135,12 +142,13 @@ function Timeinterval($compile, $templateCache, timeService) {
135 142 scope.$watch('advanced', function (newAdvanced, prevAdvanced) {
136 143 if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) {
137 144 if (!scope.advanced) {
138   - scope.intervalMs = (scope.days * 86400 +
139   - scope.hours * 3600 +
140   - scope.mins * 60 +
141   - scope.secs) * 1000;
  145 + scope.intervalMs = calculateIntervalMs();
142 146 } else {
143   - scope.setIntervalMs(scope.intervalMs);
  147 + var intervalMs = scope.intervalMs;
  148 + if (!intervalMs || isNaN(intervalMs)) {
  149 + intervalMs = calculateIntervalMs();
  150 + }
  151 + scope.setIntervalMs(intervalMs);
144 152 }
145 153 scope.updateView();
146 154 }
... ...
... ... @@ -21,7 +21,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
21 21 /*@ngInject*/
22 22 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
23 23 datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
24   - dashboardTimewindowApi, widget, deviceAliasList, widgetType) {
  24 + dashboardTimewindowApi, widget, aliasesInfo, widgetType) {
25 25
26 26 var vm = this;
27 27
... ... @@ -45,6 +45,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
45 45 var subscriptionTimewindow = null;
46 46 var dataUpdateCaf = null;
47 47
  48 + var varsRegex = /\$\{([^\}]*)\}/g;
  49 +
48 50 /*
49 51 * data = array of datasourceData
50 52 * datasourceData = {
... ... @@ -68,7 +70,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
68 70 settings: widget.config.settings,
69 71 units: widget.config.units || '',
70 72 decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
71   - datasources: widget.config.datasources,
  73 + datasources: angular.copy(widget.config.datasources),
72 74 data: [],
73 75 hiddenData: [],
74 76 timeWindow: {
... ... @@ -320,10 +322,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
320 322 $scope.legendConfig.showTotal === true);
321 323
322 324 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
323   - for (var i in widget.config.datasources) {
324   - var datasource = angular.copy(widget.config.datasources[i]);
  325 + for (var i in widgetContext.datasources) {
  326 + var datasource = widgetContext.datasources[i];
325 327 for (var a in datasource.dataKeys) {
326 328 var dataKey = datasource.dataKeys[a];
  329 + dataKey.pattern = angular.copy(dataKey.label);
327 330 var datasourceData = {
328 331 datasource: datasource,
329 332 dataKey: dataKey,
... ... @@ -333,8 +336,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
333 336 widgetContext.hiddenData.push({data: []});
334 337 if ($scope.displayLegend) {
335 338 var legendKey = {
336   - label: dataKey.label,
337   - color: dataKey.color,
  339 + dataKey: dataKey,
338 340 dataIndex: Number(i) + Number(a)
339 341 };
340 342 $scope.legendData.keys.push(legendKey);
... ... @@ -367,8 +369,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
367 369 } else if (widget.type === types.widgetType.rpc.value) {
368 370 if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
369 371 targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
370   - if (deviceAliasList[targetDeviceAliasId]) {
371   - targetDeviceId = deviceAliasList[targetDeviceAliasId].deviceId;
  372 + if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
  373 + targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
372 374 }
373 375 }
374 376 if (targetDeviceId) {
... ... @@ -402,13 +404,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
402 404 onMobileModeChanged(newIsMobile);
403 405 });
404 406
405   - $scope.$on('deviceAliasListChanged', function (event, newDeviceAliasList) {
406   - deviceAliasList = newDeviceAliasList;
  407 + $scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) {
  408 + aliasesInfo = newAliasesInfo;
407 409 if (widget.type === types.widgetType.rpc.value) {
408 410 if (targetDeviceAliasId) {
409 411 var deviceId = null;
410   - if (deviceAliasList[targetDeviceAliasId]) {
411   - deviceId = deviceAliasList[targetDeviceAliasId].deviceId;
  412 + if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
  413 + deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
412 414 }
413 415 if (!angular.equals(deviceId, targetDeviceId)) {
414 416 targetDeviceId = deviceId;
... ... @@ -609,7 +611,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
609 611 currentData.data = sourceData.data;
610 612 onDataUpdated();
611 613 if ($scope.caulculateLegendData) {
612   - updateLegend(datasourceIndex + dataKeyIndex, sourceData.data);
  614 + updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
613 615 }
614 616 }
615 617 if (apply) {
... ... @@ -617,7 +619,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
617 619 }
618 620 }
619 621
620   - function updateLegend(dataIndex, data) {
  622 + function updateLegend(dataIndex, data, apply) {
621 623 var legendKeyData = $scope.legendData.data[dataIndex];
622 624 if ($scope.legendConfig.showMin) {
623 625 legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units);
... ... @@ -631,7 +633,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
631 633 if ($scope.legendConfig.showTotal) {
632 634 legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units);
633 635 }
634   - $scope.$broadcast('legendDataUpdated');
  636 + $scope.$broadcast('legendDataUpdated', apply !== false);
635 637 }
636 638
637 639 function isNumeric(val) {
... ... @@ -707,9 +709,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
707 709 var deviceId = null;
708 710 var aliasName = null;
709 711 if (listener.datasource.type === types.datasourceType.device) {
710   - if (deviceAliasList[listener.datasource.deviceAliasId]) {
711   - deviceId = deviceAliasList[listener.datasource.deviceAliasId].deviceId;
712   - aliasName = deviceAliasList[listener.datasource.deviceAliasId].alias;
  712 + if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
  713 + deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
  714 + aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
713 715 }
714 716 if (!angular.equals(deviceId, listener.deviceId) ||
715 717 !angular.equals(aliasName, listener.datasource.name)) {
... ... @@ -756,6 +758,23 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
756 758 }
757 759 }
758 760
  761 + function updateDataKeyLabel(dataKey, deviceName, aliasName) {
  762 + var pattern = dataKey.pattern;
  763 + var label = dataKey.pattern;
  764 + var match = varsRegex.exec(pattern);
  765 + while (match !== null) {
  766 + var variable = match[0];
  767 + var variableName = match[1];
  768 + if (variableName === 'deviceName') {
  769 + label = label.split(variable).join(deviceName);
  770 + } else if (variableName === 'aliasName') {
  771 + label = label.split(variable).join(aliasName);
  772 + }
  773 + match = varsRegex.exec(pattern);
  774 + }
  775 + dataKey.label = label;
  776 + }
  777 +
759 778 function subscribe() {
760 779 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
761 780 notifyDataLoading();
... ... @@ -767,13 +786,25 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
767 786 }
768 787 }
769 788 var index = 0;
770   - for (var i in widget.config.datasources) {
771   - var datasource = widget.config.datasources[i];
  789 + for (var i in widgetContext.datasources) {
  790 + var datasource = widgetContext.datasources[i];
772 791 var deviceId = null;
773 792 if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
774   - if (deviceAliasList[datasource.deviceAliasId]) {
775   - deviceId = deviceAliasList[datasource.deviceAliasId].deviceId;
776   - datasource.name = deviceAliasList[datasource.deviceAliasId].alias;
  793 + if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
  794 + deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
  795 + datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
  796 + var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
  797 + var deviceName = '';
  798 + var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
  799 + for (var d=0;d<devicesInfo.length;d++) {
  800 + if (devicesInfo[d].id === deviceId) {
  801 + deviceName = devicesInfo[d].name;
  802 + break;
  803 + }
  804 + }
  805 + for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
  806 + updateDataKeyLabel(datasource.dataKeys[dk], deviceName, aliasName);
  807 + }
777 808 }
778 809 } else {
779 810 datasource.name = types.datasourceType.function;
... ...
... ... @@ -20,11 +20,12 @@ import deviceAliasesTemplate from './device-aliases.tpl.html';
20 20 /* eslint-enable import/no-unresolved, import/default */
21 21
22 22 /*@ngInject*/
23   -export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, widget, widgetInfo) {
  23 +export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
24 24
25 25 var vm = this;
26 26
27 27 vm.dashboard = dashboard;
  28 + vm.aliasesInfo = aliasesInfo;
28 29 vm.widget = widget;
29 30 vm.widgetInfo = widgetInfo;
30 31
... ... @@ -78,19 +79,19 @@ export default function AddWidgetController($scope, widgetService, deviceService
78 79 }
79 80
80 81 function cancel () {
81   - $mdDialog.cancel();
  82 + $mdDialog.cancel({aliasesInfo: vm.aliasesInfo});
82 83 }
83 84
84 85 function add () {
85 86 if ($scope.theForm.$valid) {
86 87 $scope.theForm.$setPristine();
87 88 vm.widget.config = vm.widgetConfig;
88   - $mdDialog.hide(vm.widget);
  89 + $mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo});
89 90 }
90 91 }
91 92
92 93 function fetchDeviceKeys (deviceAliasId, query, type) {
93   - var deviceAlias = vm.dashboard.configuration.deviceAliases[deviceAliasId];
  94 + var deviceAlias = vm.aliasesInfo.deviceAliases[deviceAliasId];
94 95 if (deviceAlias && deviceAlias.deviceId) {
95 96 return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
96 97 } else {
... ... @@ -101,7 +102,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
101 102 function createDeviceAlias (event, alias) {
102 103
103 104 var deferred = $q.defer();
104   - var singleDeviceAlias = {id: null, alias: alias, deviceId: null};
  105 + var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
105 106
106 107 $mdDialog.show({
107 108 controller: 'DeviceAliasesController',
... ... @@ -111,7 +112,7 @@ export default function AddWidgetController($scope, widgetService, deviceService
111 112 config: {
112 113 deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
113 114 widgets: null,
114   - isSingleDevice: true,
  115 + isSingleDeviceAlias: true,
115 116 singleDeviceAlias: singleDeviceAlias
116 117 }
117 118 },
... ... @@ -121,8 +122,15 @@ export default function AddWidgetController($scope, widgetService, deviceService
121 122 targetEvent: event
122 123 }).then(function (singleDeviceAlias) {
123 124 vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
124   - { alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId };
125   - deferred.resolve(singleDeviceAlias);
  125 + { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
  126 + deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases).then(
  127 + function(resolution) {
  128 + if (!resolution.error) {
  129 + vm.aliasesInfo = resolution.aliasesInfo;
  130 + }
  131 + deferred.resolve(singleDeviceAlias);
  132 + }
  133 + );
126 134 }, function () {
127 135 deferred.reject();
128 136 });
... ...
... ... @@ -37,7 +37,7 @@
37 37 ng-model="vm.widgetConfig"
38 38 widget-settings-schema="vm.settingsSchema"
39 39 datakey-settings-schema="vm.dataKeySettingsSchema"
40   - device-aliases="vm.dashboard.configuration.deviceAliases"
  40 + device-aliases="vm.aliasesInfo.deviceAliases"
41 41 functions-only="vm.functionsOnly"
42 42 fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
43 43 on-create-device-alias="vm.createDeviceAlias(event, alias)"
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<section class="tb-aliases-device-select" layout='row' layout-align="start center" ng-style="{minHeight: '32px', padding: '0 6px'}">
  20 + <md-button class="md-icon-button" aria-label="{{ 'dashboard.select-devices' | translate }}" ng-click="openEditMode($event)">
  21 + <md-tooltip md-direction="{{tooltipDirection}}">
  22 + {{ 'dashboard.select-devices' | translate }}
  23 + </md-tooltip>
  24 + <md-icon aria-label="{{ 'dashboard.select-devices' | translate }}" class="material-icons">devices_other</md-icon>
  25 + </md-button>
  26 + <span ng-click="openEditMode($event)">
  27 + <md-tooltip md-direction="{{tooltipDirection}}">
  28 + {{ 'dashboard.select-devices' | translate }}
  29 + </md-tooltip>
  30 + {{displayValue}}
  31 + </span>
  32 +</section>
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/*@ngInject*/
  18 +export default function AliasesDeviceSelectPanelController(mdPanelRef, $scope, types, deviceAliases, deviceAliasesInfo, onDeviceAliasesUpdate) {
  19 +
  20 + var vm = this;
  21 + vm._mdPanelRef = mdPanelRef;
  22 + vm.deviceAliases = deviceAliases;
  23 + vm.deviceAliasesInfo = deviceAliasesInfo;
  24 + vm.onDeviceAliasesUpdate = onDeviceAliasesUpdate;
  25 +
  26 + $scope.$watch('vm.deviceAliases', function () {
  27 + if (onDeviceAliasesUpdate) {
  28 + onDeviceAliasesUpdate(vm.deviceAliases);
  29 + }
  30 + }, true);
  31 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-content flex layout="column">
  19 + <section flex layout="column">
  20 + <md-content flex class="md-padding" layout="column">
  21 + <div flex layout="row" ng-repeat="(aliasId, deviceAlias) in vm.deviceAliases">
  22 + <md-input-container flex>
  23 + <label>{{deviceAlias.alias}}</label>
  24 + <md-select ng-model="vm.deviceAliases[aliasId].deviceId">
  25 + <md-option ng-repeat="deviceInfo in vm.deviceAliasesInfo[aliasId]" ng-value="deviceInfo.id">
  26 + {{deviceInfo.name}}
  27 + </md-option>
  28 + </md-select>
  29 + </md-input-container>
  30 + </div>
  31 + </md-content>
  32 + </section>
  33 +</md-content>
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './aliases-device-select.scss';
  18 +
  19 +import $ from 'jquery';
  20 +
  21 +/* eslint-disable import/no-unresolved, import/default */
  22 +
  23 +import aliasesDeviceSelectButtonTemplate from './aliases-device-select-button.tpl.html';
  24 +import aliasesDeviceSelectPanelTemplate from './aliases-device-select-panel.tpl.html';
  25 +
  26 +/* eslint-enable import/no-unresolved, import/default */
  27 +
  28 +/* eslint-disable angular/angularelement */
  29 +/*@ngInject*/
  30 +export default function AliasesDeviceSelectDirective($compile, $templateCache, types, $mdPanel, $document, $translate) {
  31 +
  32 + var linker = function (scope, element, attrs, ngModelCtrl) {
  33 +
  34 + /* tbAliasesDeviceSelect (ng-model)
  35 + * {
  36 + * "aliasId": {
  37 + * alias: alias,
  38 + * deviceId: deviceId
  39 + * }
  40 + * }
  41 + */
  42 +
  43 + var template = $templateCache.get(aliasesDeviceSelectButtonTemplate);
  44 +
  45 + scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top';
  46 +
  47 + element.html(template);
  48 +
  49 + scope.openEditMode = function (event) {
  50 + if (scope.disabled) {
  51 + return;
  52 + }
  53 + var position;
  54 + var panelHeight = 250;
  55 + var panelWidth = 300;
  56 + var offset = element[0].getBoundingClientRect();
  57 + var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
  58 + var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
  59 + var yPosition;
  60 + var xPosition;
  61 + if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
  62 + yPosition = $mdPanel.yPosition.ABOVE;
  63 + } else {
  64 + yPosition = $mdPanel.yPosition.BELOW;
  65 + }
  66 + if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
  67 + xPosition = $mdPanel.xPosition.ALIGN_END;
  68 + } else {
  69 + xPosition = $mdPanel.xPosition.ALIGN_START;
  70 + }
  71 + position = $mdPanel.newPanelPosition()
  72 + .relativeTo(element)
  73 + .addPanelPosition(xPosition, yPosition);
  74 + var config = {
  75 + attachTo: angular.element($document[0].body),
  76 + controller: 'AliasesDeviceSelectPanelController',
  77 + controllerAs: 'vm',
  78 + templateUrl: aliasesDeviceSelectPanelTemplate,
  79 + panelClass: 'tb-aliases-device-select-panel',
  80 + position: position,
  81 + fullscreen: false,
  82 + locals: {
  83 + 'deviceAliases': angular.copy(scope.model),
  84 + 'deviceAliasesInfo': scope.deviceAliasesInfo,
  85 + 'onDeviceAliasesUpdate': function (deviceAliases) {
  86 + scope.model = deviceAliases;
  87 + scope.updateView();
  88 + }
  89 + },
  90 + openFrom: event,
  91 + clickOutsideToClose: true,
  92 + escapeToClose: true,
  93 + focusOnOpen: false
  94 + };
  95 + $mdPanel.open(config);
  96 + }
  97 +
  98 + scope.updateView = function () {
  99 + var value = angular.copy(scope.model);
  100 + ngModelCtrl.$setViewValue(value);
  101 + updateDisplayValue();
  102 + }
  103 +
  104 + ngModelCtrl.$render = function () {
  105 + if (ngModelCtrl.$viewValue) {
  106 + var value = ngModelCtrl.$viewValue;
  107 + scope.model = angular.copy(value);
  108 + updateDisplayValue();
  109 + }
  110 + }
  111 +
  112 + function updateDisplayValue() {
  113 + var displayValue;
  114 + var singleValue = true;
  115 + var currentAliasId;
  116 + for (var aliasId in scope.model) {
  117 + if (!currentAliasId) {
  118 + currentAliasId = aliasId;
  119 + } else {
  120 + singleValue = false;
  121 + break;
  122 + }
  123 + }
  124 + if (singleValue) {
  125 + var deviceId = scope.model[currentAliasId].deviceId;
  126 + var devicesInfo = scope.deviceAliasesInfo[currentAliasId];
  127 + for (var i=0;i<devicesInfo.length;i++) {
  128 + if (devicesInfo[i].id === deviceId) {
  129 + displayValue = devicesInfo[i].name;
  130 + break;
  131 + }
  132 + }
  133 + } else {
  134 + displayValue = $translate.instant('device.devices');
  135 + }
  136 + scope.displayValue = displayValue;
  137 + }
  138 +
  139 + $compile(element.contents())(scope);
  140 + }
  141 +
  142 + return {
  143 + restrict: "E",
  144 + require: "^ngModel",
  145 + scope: {
  146 + deviceAliasesInfo:'='
  147 + },
  148 + link: linker
  149 + };
  150 +
  151 +}
  152 +
  153 +/* eslint-enable angular/angularelement */
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.md-panel {
  18 + &.tb-aliases-device-select-panel {
  19 + position: absolute;
  20 + }
  21 +}
  22 +
  23 +.tb-aliases-device-select-panel {
  24 + max-height: 250px;
  25 + min-width: 300px;
  26 + background: white;
  27 + border-radius: 4px;
  28 + box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
  29 + 0 13px 19px 2px rgba(0, 0, 0, 0.14),
  30 + 0 5px 24px 4px rgba(0, 0, 0, 0.12);
  31 + overflow-x: hidden;
  32 + overflow-y: auto;
  33 + md-content {
  34 + background-color: #fff;
  35 + }
  36 +}
  37 +
  38 +.tb-aliases-device-select {
  39 + span {
  40 + pointer-events: all;
  41 + cursor: pointer;
  42 + }
  43 +}
... ...
... ... @@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
23 23
24 24 /*@ngInject*/
25 25 export default function DashboardController(types, widgetService, userService,
26   - dashboardService, timeService, itembuffer, importExport, hotkeys, $window, $rootScope,
  26 + dashboardService, timeService, deviceService, itembuffer, importExport, hotkeys, $window, $rootScope,
27 27 $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
28 28
29 29 var user = userService.getCurrentUser();
... ... @@ -82,6 +82,8 @@ export default function DashboardController(types, widgetService, userService,
82 82 vm.loadDashboard = loadDashboard;
83 83 vm.getServerTimeDiff = getServerTimeDiff;
84 84 vm.noData = noData;
  85 + vm.dashboardConfigurationError = dashboardConfigurationError;
  86 + vm.showDashboardToolbar = showDashboardToolbar;
85 87 vm.onAddWidgetClosed = onAddWidgetClosed;
86 88 vm.onEditWidgetClosed = onEditWidgetClosed;
87 89 vm.openDeviceAliases = openDeviceAliases;
... ... @@ -212,9 +214,21 @@ export default function DashboardController(types, widgetService, userService,
212 214 if (angular.isUndefined(vm.dashboard.configuration.timewindow)) {
213 215 vm.dashboard.configuration.timewindow = timeService.defaultTimewindow();
214 216 }
215   - vm.dashboardConfiguration = vm.dashboard.configuration;
216   - vm.widgets = vm.dashboard.configuration.widgets;
217   - deferred.resolve();
  217 + deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
  218 + .then(
  219 + function(resolution) {
  220 + if (resolution.error && !isTenantAdmin()) {
  221 + vm.configurationError = true;
  222 + showAliasesResolutionError(resolution.error);
  223 + deferred.reject();
  224 + } else {
  225 + vm.aliasesInfo = resolution.aliasesInfo;
  226 + vm.dashboardConfiguration = vm.dashboard.configuration;
  227 + vm.widgets = vm.dashboard.configuration.widgets;
  228 + deferred.resolve();
  229 + }
  230 + }
  231 + );
218 232 }, function fail(e) {
219 233 deferred.reject(e);
220 234 });
... ... @@ -245,7 +259,15 @@ export default function DashboardController(types, widgetService, userService,
245 259 }
246 260
247 261 function noData() {
248   - return vm.dashboardInitComplete && vm.widgets.length == 0;
  262 + return vm.dashboardInitComplete && !vm.configurationError && vm.widgets.length == 0;
  263 + }
  264 +
  265 + function dashboardConfigurationError() {
  266 + return vm.dashboardInitComplete && vm.configurationError;
  267 + }
  268 +
  269 + function showDashboardToolbar() {
  270 + return vm.dashboardInitComplete && !vm.configurationError;
249 271 }
250 272
251 273 function openDeviceAliases($event) {
... ... @@ -257,7 +279,7 @@ export default function DashboardController(types, widgetService, userService,
257 279 config: {
258 280 deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
259 281 widgets: vm.widgets,
260   - isSingleDevice: false,
  282 + isSingleDeviceAlias: false,
261 283 singleDeviceAlias: null
262 284 }
263 285 },
... ... @@ -267,6 +289,7 @@ export default function DashboardController(types, widgetService, userService,
267 289 targetEvent: $event
268 290 }).then(function (deviceAliases) {
269 291 vm.dashboard.configuration.deviceAliases = deviceAliases;
  292 + deviceAliasesUpdated();
270 293 }, function () {
271 294 });
272 295 }
... ... @@ -331,7 +354,7 @@ export default function DashboardController(types, widgetService, userService,
331 354
332 355 function importWidget($event) {
333 356 $event.stopPropagation();
334   - importExport.importWidget($event, vm.dashboard);
  357 + importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated);
335 358 }
336 359
337 360 function widgetMouseDown($event, widget) {
... ... @@ -433,7 +456,7 @@ export default function DashboardController(types, widgetService, userService,
433 456
434 457 function pasteWidget($event) {
435 458 var pos = vm.dashboardContainer.getEventGridPosition($event);
436   - itembuffer.pasteWidget(vm.dashboard, pos);
  459 + itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated);
437 460 }
438 461
439 462 function prepareWidgetContextMenu() {
... ... @@ -570,7 +593,7 @@ export default function DashboardController(types, widgetService, userService,
570 593 controller: 'AddWidgetController',
571 594 controllerAs: 'vm',
572 595 templateUrl: addWidgetTemplate,
573   - locals: {dashboard: vm.dashboard, widget: newWidget, widgetInfo: widgetTypeInfo},
  596 + locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo},
574 597 parent: angular.element($document[0].body),
575 598 fullscreen: true,
576 599 skipHide: true,
... ... @@ -579,7 +602,9 @@ export default function DashboardController(types, widgetService, userService,
579 602 var w = angular.element($window);
580 603 w.triggerHandler('resize');
581 604 }
582   - }).then(function (widget) {
  605 + }).then(function (result) {
  606 + var widget = result.widget;
  607 + vm.aliasesInfo = result.aliasesInfo;
583 608 var columns = 24;
584 609 if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
585 610 columns = vm.dashboard.configuration.gridSettings.columns;
... ... @@ -590,7 +615,8 @@ export default function DashboardController(types, widgetService, userService,
590 615 widget.sizeY *= ratio;
591 616 }
592 617 vm.widgets.push(widget);
593   - }, function () {
  618 + }, function (rejection) {
  619 + vm.aliasesInfo = rejection.aliasesInfo;
594 620 });
595 621 }
596 622 );
... ... @@ -634,6 +660,7 @@ export default function DashboardController(types, widgetService, userService,
634 660 vm.dashboard = vm.prevDashboard;
635 661 vm.widgets = vm.dashboard.configuration.widgets;
636 662 vm.dashboardConfiguration = vm.dashboard.configuration;
  663 + deviceAliasesUpdated();
637 664 }
638 665 }
639 666 }
... ... @@ -648,6 +675,31 @@ export default function DashboardController(types, widgetService, userService,
648 675 notifyDashboardUpdated();
649 676 }
650 677
  678 + function showAliasesResolutionError(error) {
  679 + var alert = $mdDialog.alert()
  680 + .parent(angular.element($document[0].body))
  681 + .clickOutsideToClose(true)
  682 + .title($translate.instant('dashboard.alias-resolution-error-title'))
  683 + .htmlContent($translate.instant(error))
  684 + .ariaLabel($translate.instant('dashboard.alias-resolution-error-title'))
  685 + .ok($translate.instant('action.close'))
  686 + alert._options.skipHide = true;
  687 + alert._options.fullscreen = true;
  688 +
  689 + $mdDialog.show(alert);
  690 + }
  691 +
  692 + function deviceAliasesUpdated() {
  693 + deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
  694 + .then(
  695 + function(resolution) {
  696 + if (resolution.aliasesInfo) {
  697 + vm.aliasesInfo = resolution.aliasesInfo;
  698 + }
  699 + }
  700 + );
  701 + }
  702 +
651 703 function notifyDashboardUpdated() {
652 704 if (vm.widgetEditMode) {
653 705 var parentScope = $window.parent.angular.element($window.frameElement).scope();
... ...
... ... @@ -113,13 +113,11 @@ section.tb-dashboard-toolbar {
113 113 min-height: 36px;
114 114 height: 36px;
115 115 md-fab-actions {
  116 + font-size: 16px;
116 117 margin-top: 0px;
117 118 .close-action {
118 119 margin-right: -18px;
119 120 }
120   - tb-timewindow {
121   - font-size: 16px;
122   - }
123 121 }
124 122 }
125 123 }
... ...
... ... @@ -23,7 +23,7 @@
23 23 'background-attachment': 'scroll',
24 24 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
25 25 'background-position': '0% 0%'}">
26   - <section class="tb-dashboard-toolbar"
  26 + <section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
27 27 ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
28 28 <md-fab-toolbar md-open="vm.toolbarOpened"
29 29 md-direction="left">
... ... @@ -49,6 +49,11 @@
49 49 </md-button>
50 50 <tb-timewindow direction="left" tooltip-direction="bottom" aggregation ng-model="vm.dashboardConfiguration.timewindow">
51 51 </tb-timewindow>
  52 + <tb-aliases-device-select ng-show="!vm.isEdit"
  53 + tooltip-direction="bottom"
  54 + ng-model="vm.aliasesInfo.deviceAliases"
  55 + device-aliases-info="vm.aliasesInfo.deviceAliasesInfo">
  56 + </tb-aliases-device-select>
52 57 <md-button ng-show="vm.isEdit" aria-label="{{ 'device.aliases' | translate }}" class="md-icon-button"
53 58 ng-click="vm.openDeviceAliases($event)">
54 59 <md-tooltip md-direction="bottom">
... ... @@ -70,17 +75,27 @@
70 75 <section class="tb-dashboard-container tb-absolute-fill"
71 76 ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
72 77 <section ng-show="!loading && vm.noData()" layout-align="center center"
  78 + ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
73 79 ng-class="{'tb-padded' : !vm.widgetEditMode}"
74 80 style="text-transform: uppercase; display: flex; z-index: 1;"
75 81 class="md-headline tb-absolute-fill">
76   - <span translate ng-if="!vm.isEdit">
77   - dashboard.no-widgets
78   - </span>
  82 + <span translate ng-if="!vm.isEdit">
  83 + dashboard.no-widgets
  84 + </span>
79 85 <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)">
80 86 <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
81 87 {{ 'dashboard.add-widget' | translate }}
82 88 </md-button>
83 89 </section>
  90 + <section ng-show="!loading && vm.dashboardConfigurationError()" layout-align="center center"
  91 + ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
  92 + ng-class="{'tb-padded' : !vm.widgetEditMode}"
  93 + style="text-transform: uppercase; display: flex; z-index: 1;"
  94 + class="md-headline tb-absolute-fill">
  95 + <span translate>
  96 + dashboard.configuration-error
  97 + </span>
  98 + </section>
84 99 <section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center"
85 100 ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
86 101 <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
... ... @@ -101,7 +116,7 @@
101 116 widgets="vm.widgets"
102 117 columns="vm.dashboard.configuration.gridSettings.columns"
103 118 margins="vm.dashboard.configuration.gridSettings.margins"
104   - device-alias-list="vm.dashboard.configuration.deviceAliases"
  119 + aliases-info="vm.aliasesInfo"
105 120 dashboard-timewindow="vm.dashboardConfiguration.timewindow"
106 121 is-edit="vm.isEdit"
107 122 is-mobile="vm.forceDashboardMobileMode"
... ... @@ -139,6 +154,7 @@
139 154 <form name="vm.widgetForm" ng-if="vm.isEditingWidget">
140 155 <tb-edit-widget
141 156 dashboard="vm.dashboard"
  157 + aliases-info="vm.aliasesInfo"
142 158 widget="vm.editingWidget"
143 159 the-form="vm.widgetForm">
144 160 </tb-edit-widget>
... ...
... ... @@ -17,25 +17,23 @@ import './device-aliases.scss';
17 17
18 18 /*@ngInject*/
19 19 export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
20   - types, config) {
  20 + types, config) {
21 21
22 22 var vm = this;
23 23
24   - vm.isSingleDevice = config.isSingleDevice;
  24 + vm.isSingleDeviceAlias = config.isSingleDeviceAlias;
25 25 vm.singleDeviceAlias = config.singleDeviceAlias;
26 26 vm.deviceAliases = [];
27   - vm.singleDevice = null;
28   - vm.singleDeviceSearchText = '';
29 27 vm.title = config.customTitle ? config.customTitle : 'device.aliases';
30 28 vm.disableAdd = config.disableAdd;
31 29 vm.aliasToWidgetsMap = {};
32 30
  31 +
  32 + vm.onFilterDeviceChanged = onFilterDeviceChanged;
33 33 vm.addAlias = addAlias;
34   - vm.cancel = cancel;
35   - vm.deviceSearchTextChanged = deviceSearchTextChanged;
36   - vm.deviceChanged = deviceChanged;
37   - vm.fetchDevices = fetchDevices;
38 34 vm.removeAlias = removeAlias;
  35 +
  36 + vm.cancel = cancel;
39 37 vm.save = save;
40 38
41 39 initController();
... ... @@ -80,40 +78,46 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
80 78 }
81 79 }
82 80
83   - for (aliasId in config.deviceAliases) {
84   - var alias = config.deviceAliases[aliasId].alias;
85   - var deviceId = config.deviceAliases[aliasId].deviceId;
86   - var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
87   - if (deviceId) {
88   - fetchAliasDevice(deviceAlias, deviceId);
  81 + if (vm.isSingleDeviceAlias) {
  82 + if (!vm.singleDeviceAlias.deviceFilter || vm.singleDeviceAlias.deviceFilter == null) {
  83 + vm.singleDeviceAlias.deviceFilter = {
  84 + useFilter: false,
  85 + deviceNameFilter: '',
  86 + deviceList: [],
  87 + };
89 88 }
90   - vm.deviceAliases.push(deviceAlias);
91 89 }
92   - }
93   -
94   - function fetchDevices(searchText) {
95   - var pageLink = {limit: 10, textSearch: searchText};
96 90
97   - var deferred = $q.defer();
98   -
99   - deviceService.getTenantDevices(pageLink).then(function success(result) {
100   - deferred.resolve(result.data);
101   - }, function fail() {
102   - deferred.reject();
103   - });
104   -
105   - return deferred.promise;
106   - }
107   -
108   - function deviceSearchTextChanged() {
  91 + for (aliasId in config.deviceAliases) {
  92 + var deviceAlias = config.deviceAliases[aliasId];
  93 + var alias = deviceAlias.alias;
  94 + var deviceFilter;
  95 + if (!deviceAlias.deviceFilter) {
  96 + deviceFilter = {
  97 + useFilter: false,
  98 + deviceNameFilter: '',
  99 + deviceList: [],
  100 + };
  101 + if (deviceAlias.deviceId) {
  102 + deviceFilter.deviceList = [deviceAlias.deviceId];
  103 + } else {
  104 + deviceFilter.deviceList = [];
  105 + }
  106 + } else {
  107 + deviceFilter = deviceAlias.deviceFilter;
  108 + }
  109 + var result = {id: aliasId, alias: alias, deviceFilter: deviceFilter, changed: true};
  110 + vm.deviceAliases.push(result);
  111 + }
109 112 }
110 113
111   - function deviceChanged(deviceAlias) {
112   - if (deviceAlias && deviceAlias.device) {
113   - if (angular.isDefined(deviceAlias.changed) && !deviceAlias.changed) {
114   - deviceAlias.changed = true;
115   - } else {
116   - deviceAlias.alias = deviceAlias.device.name;
  114 + function onFilterDeviceChanged(device, deviceAlias) {
  115 + if (deviceAlias) {
  116 + if (!deviceAlias.alias || deviceAlias.alias.length == 0) {
  117 + deviceAlias.changed = false;
  118 + }
  119 + if (!deviceAlias.changed && device) {
  120 + deviceAlias.alias = device.name;
117 121 }
118 122 }
119 123 }
... ... @@ -124,7 +128,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
124 128 aliasId = Math.max(vm.deviceAliases[a].id, aliasId);
125 129 }
126 130 aliasId++;
127   - var deviceAlias = {id: aliasId, alias: '', device: null, searchText: ''};
  131 + var deviceAlias = {id: aliasId, alias: '', deviceFilter: {useFilter: false, deviceNameFilter: '', deviceList: []}, changed: false};
128 132 vm.deviceAliases.push(deviceAlias);
129 133 }
130 134
... ... @@ -150,9 +154,6 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
150 154
151 155 $mdDialog.show(alert);
152 156 } else {
153   - for (var i = index + 1; i < vm.deviceAliases.length; i++) {
154   - vm.deviceAliases[i].changed = false;
155   - }
156 157 vm.deviceAliases.splice(index, 1);
157 158 if ($scope.theForm) {
158 159 $scope.theForm.$setDirty();
... ... @@ -165,6 +166,15 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
165 166 $mdDialog.cancel();
166 167 }
167 168
  169 + function cleanupDeviceFilter(deviceFilter) {
  170 + if (deviceFilter.useFilter) {
  171 + deviceFilter.deviceList = [];
  172 + } else {
  173 + deviceFilter.deviceNameFilter = '';
  174 + }
  175 + return deviceFilter;
  176 + }
  177 +
168 178 function save() {
169 179
170 180 var deviceAliases = {};
... ... @@ -175,9 +185,9 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
175 185 var alias;
176 186 var i;
177 187
178   - if (vm.isSingleDevice) {
  188 + if (vm.isSingleDeviceAlias) {
179 189 maxAliasId = 0;
180   - vm.singleDeviceAlias.deviceId = vm.singleDevice.id.id;
  190 + vm.singleDeviceAlias.deviceFilter = cleanupDeviceFilter(vm.singleDeviceAlias.deviceFilter);
181 191 for (i in vm.deviceAliases) {
182 192 aliasId = vm.deviceAliases[i].id;
183 193 alias = vm.deviceAliases[i].alias;
... ... @@ -195,7 +205,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
195 205 alias = vm.deviceAliases[i].alias;
196 206 if (!uniqueAliasList[alias]) {
197 207 uniqueAliasList[alias] = alias;
198   - deviceAliases[aliasId] = {alias: alias, deviceId: vm.deviceAliases[i].device.id.id};
  208 + deviceAliases[aliasId] = {alias: alias, deviceFilter: cleanupDeviceFilter(vm.deviceAliases[i].deviceFilter)};
199 209 } else {
200 210 valid = false;
201 211 break;
... ... @@ -204,7 +214,7 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
204 214 }
205 215 if (valid) {
206 216 $scope.theForm.$setPristine();
207   - if (vm.isSingleDevice) {
  217 + if (vm.isSingleDeviceAlias) {
208 218 $mdDialog.hide(vm.singleDeviceAlias);
209 219 } else {
210 220 $mdDialog.hide(deviceAliases);
... ... @@ -214,11 +224,4 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
214 224 }
215 225 }
216 226
217   - function fetchAliasDevice(deviceAlias, deviceId) {
218   - deviceService.getDevice(deviceId).then(function (device) {
219   - deviceAlias.device = device;
220   - deviceAlias.searchText = device.name;
221   - });
222   - }
223   -
224 227 }
... ...
... ... @@ -13,20 +13,13 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
16   -.tb-alias {
17   - padding: 10px 0 0 10px;
18   - margin: 5px;
19 16
20   - md-input-container {
21   - margin: 0;
  17 +.tb-aliases-dialog {
  18 + .md-dialog-content {
  19 + padding-bottom: 0px;
22 20 }
23   - md-autocomplete {
24   - height: 30px;
25   - md-autocomplete-wrap {
26   - height: 30px;
27   - }
28   - input, input:not(.md-input) {
29   - height: 30px;
30   - }
  21 + .tb-alias {
  22 + padding: 10px 0 0 10px;
  23 + margin: 5px;
31 24 }
32 25 }
... ...
... ... @@ -15,117 +15,80 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<md-dialog style="width: 700px;" aria-label="{{ vm.title | translate }}">
  18 +<md-dialog class="tb-aliases-dialog" style="width: 700px;" aria-label="{{ vm.title | translate }}">
19 19 <form name="theForm" ng-submit="vm.save()">
20   - <md-toolbar>
21   - <div class="md-toolbar-tools">
22   - <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</h2>
23   - <span flex></span>
24   - <md-button class="md-icon-button" ng-click="vm.cancel()">
25   - <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
26   - </md-button>
27   - </div>
28   - </md-toolbar>
29   - <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
30   - <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
31   - <md-dialog-content>
32   - <div class="md-dialog-content">
33   - <fieldset ng-disabled="loading">
34   - <div ng-show="vm.isSingleDevice">
35   - <md-autocomplete
36   - ng-required="vm.isSingleDevice"
37   - md-input-name="device_id"
38   - ng-model="vm.singleDevice"
39   - md-selected-item="vm.singleDevice"
40   - md-search-text="vm.singleDeviceSearchText"
41   - md-search-text-change="vm.deviceSearchTextChanged(vm.singleDevice)"
42   - md-items="item in vm.fetchDevices(vm.singleDeviceSearchText)"
43   - md-item-text="item.name"
44   - md-min-length="0"
45   - placeholder="{{ 'device.device' | translate }}">
46   - <md-item-template>
47   - <span md-highlight-text="vm.singleDeviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
48   - </md-item-template>
49   - <md-not-found>
50   - <span translate translate-values='{ device: vm.singleDeviceSearchText }'>device.no-devices-matching</span>
51   - </md-not-found>
52   - <div ng-messages="theForm.device_id.$error">
53   - <div translate ng-message="required">device.device-required</div>
54   - </div>
55   - </md-autocomplete>
56   - </div>
57   - <div ng-show="!vm.isSingleDevice" flex layout="row" layout-align="start center">
58   - <span flex="5"></span>
59   - <div flex layout="row" layout-align="start center"
60   - style="padding: 0 0 0 10px; margin: 5px;">
61   - <span translate flex="40" style="min-width: 100px;">device.alias</span>
62   - <span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.device</span>
63   - <span style="min-width: 40px;"></span>
64   - </div>
65   - </div>
66   - <div ng-show="!vm.isSingleDevice" style="max-height: 300px; overflow: auto; padding-bottom: 20px;">
67   - <div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index">
68   - <span flex="5">{{$index + 1}}.</span>
69   - <div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
70   - <md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block">
71   - <input required name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias">
72   - <div ng-messages="aliasForm.alias.$error">
73   - <div translate ng-message="required">device.alias-required</div>
74   - </div>
75   - </md-input-container>
76   - <section flex="60" layout="column">
77   - <md-autocomplete flex
78   - ng-required="!vm.isSingleDevice"
79   - md-input-name="device_id"
80   - ng-model="deviceAlias.device"
81   - md-selected-item="deviceAlias.device"
82   - md-search-text="deviceAlias.searchText"
83   - md-search-text-change="vm.deviceSearchTextChanged(deviceAlias)"
84   - md-selected-item-change="vm.deviceChanged(deviceAlias)"
85   - md-items="item in vm.fetchDevices(deviceAlias.searchText)"
86   - md-item-text="item.name"
87   - md-min-length="0"
88   - placeholder="{{ 'device.device' | translate }}">
89   - <md-item-template>
90   - <span md-highlight-text="deviceAlias.searchText" md-highlight-flags="^i">{{item.name}}</span>
91   - </md-item-template>
92   - <md-not-found>
93   - <span translate translate-values='{ device: deviceAlias.searchText }'>device.no-devices-matching</span>
94   - </md-not-found>
95   - </md-autocomplete>
96   - <div class="tb-error-messages" ng-messages="aliasForm.device_id.$error">
97   - <div translate ng-message="required" class="tb-error-message">device.device-required</div>
98   - </div>
99   - </section>
100   - <md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
101   - ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
102   - <md-tooltip md-direction="top">
103   - {{ 'device.remove-alias' | translate }}
104   - </md-tooltip>
105   - <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
106   - close
107   - </md-icon>
108   - </md-button>
109   - </div>
110   - </div>
111   - </div>
112   - <div ng-show="!vm.isSingleDevice && !vm.disableAdd" style="padding-bottom: 10px;">
113   - <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
114   - <md-tooltip md-direction="top">
115   - {{ 'device.add-alias' | translate }}
116   - </md-tooltip>
117   - <span translate>action.add</span>
118   - </md-button>
119   - </div>
120   - </fieldset>
121   - </div>
122   - </md-dialog-content>
123   - <md-dialog-actions layout="row">
124   - <span flex></span>
125   - <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
126   - {{ 'action.save' | translate }}
127   - </md-button>
128   - <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
129   - </md-dialog-actions>
130   - </form>
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2>{{ vm.isSingleDeviceAlias ? ('device.configure-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  25 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  26 + </md-button>
  27 + </div>
  28 + </md-toolbar>
  29 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
  30 + <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
  31 + <md-dialog-content>
  32 + <div class="md-dialog-content">
  33 + <fieldset ng-disabled="loading">
  34 + <div ng-show="vm.isSingleDeviceAlias">
  35 + <tb-device-filter ng-model="vm.singleDeviceAlias.deviceFilter">
  36 + </tb-device-filter>
  37 + </div>
  38 + <div ng-show="!vm.isSingleDeviceAlias" flex layout="row" layout-align="start center">
  39 + <span flex="5"></span>
  40 + <div flex layout="row" layout-align="start center"
  41 + style="padding: 0 0 0 10px; margin: 5px;">
  42 + <span translate flex="40" style="min-width: 100px;">device.alias</span>
  43 + <span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.devices</span>
  44 + <span style="min-width: 40px;"></span>
  45 + </div>
  46 + </div>
  47 + <div ng-show="!vm.isSingleDeviceAlias" style="max-height: 500px; overflow: auto; padding-bottom: 20px;">
  48 + <div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index">
  49 + <span flex="5">{{$index + 1}}.</span>
  50 + <div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
  51 + <md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block">
  52 + <input required ng-change="deviceAlias.changed=true" name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias">
  53 + <div ng-messages="aliasForm.alias.$error">
  54 + <div translate ng-message="required">device.alias-required</div>
  55 + </div>
  56 + </md-input-container>
  57 + <section flex="60" layout="column">
  58 + <tb-device-filter style="padding-left: 10px;"
  59 + ng-model="deviceAlias.deviceFilter"
  60 + on-matching-device-change="vm.onFilterDeviceChanged(device, deviceAlias)">
  61 + </tb-device-filter>
  62 + </section>
  63 + <md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
  64 + ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
  65 + <md-tooltip md-direction="top">
  66 + {{ 'device.remove-alias' | translate }}
  67 + </md-tooltip>
  68 + <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
  69 + close
  70 + </md-icon>
  71 + </md-button>
  72 + </div>
  73 + </div>
  74 + </div>
  75 + <div ng-show="!vm.isSingleDeviceAlias && !vm.disableAdd" style="padding-bottom: 10px;">
  76 + <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
  77 + <md-tooltip md-direction="top">
  78 + {{ 'device.add-alias' | translate }}
  79 + </md-tooltip>
  80 + <span translate>action.add</span>
  81 + </md-button>
  82 + </div>
  83 + </fieldset>
  84 + </div>
  85 + </md-dialog-content>
  86 + <md-dialog-actions layout="row">
  87 + <span flex></span>
  88 + <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
  89 + {{ 'action.save' | translate }}
  90 + </md-button>
  91 + <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
  92 + </md-dialog-actions>
  93 + </form>
131 94 </md-dialog>
\ No newline at end of file
... ...
... ... @@ -58,7 +58,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
58 58 });
59 59
60 60 scope.fetchDeviceKeys = function (deviceAliasId, query, type) {
61   - var deviceAlias = scope.dashboard.configuration.deviceAliases[deviceAliasId];
  61 + var deviceAlias = scope.aliasesInfo.deviceAliases[deviceAliasId];
62 62 if (deviceAlias && deviceAlias.deviceId) {
63 63 return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
64 64 } else {
... ... @@ -69,7 +69,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
69 69 scope.createDeviceAlias = function (event, alias) {
70 70
71 71 var deferred = $q.defer();
72   - var singleDeviceAlias = {id: null, alias: alias, deviceId: null};
  72 + var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
73 73
74 74 $mdDialog.show({
75 75 controller: 'DeviceAliasesController',
... ... @@ -79,7 +79,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
79 79 config: {
80 80 deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
81 81 widgets: null,
82   - isSingleDevice: true,
  82 + isSingleDeviceAlias: true,
83 83 singleDeviceAlias: singleDeviceAlias
84 84 }
85 85 },
... ... @@ -89,8 +89,15 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
89 89 targetEvent: event
90 90 }).then(function (singleDeviceAlias) {
91 91 scope.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
92   - { alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId };
93   - deferred.resolve(singleDeviceAlias);
  92 + { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
  93 + deviceService.processDeviceAliases(scope.dashboard.configuration.deviceAliases).then(
  94 + function(resolution) {
  95 + if (!resolution.error) {
  96 + scope.aliasesInfo = resolution.aliasesInfo;
  97 + }
  98 + deferred.resolve(singleDeviceAlias);
  99 + }
  100 + );
94 101 }, function () {
95 102 deferred.reject();
96 103 });
... ... @@ -106,6 +113,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
106 113 link: linker,
107 114 scope: {
108 115 dashboard: '=',
  116 + aliasesInfo: '=',
109 117 widget: '=',
110 118 theForm: '='
111 119 }
... ...
... ... @@ -20,7 +20,7 @@
20 20 ng-model="widgetConfig"
21 21 widget-settings-schema="settingsSchema"
22 22 datakey-settings-schema="dataKeySettingsSchema"
23   - device-aliases="dashboard.configuration.deviceAliases"
  23 + device-aliases="aliasesInfo.deviceAliases"
24 24 functions-only="functionsOnly"
25 25 fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)"
26 26 on-create-device-alias="createDeviceAlias(event, alias)"
... ...
... ... @@ -24,6 +24,7 @@ import thingsboardApiUser from '../api/user.service';
24 24 import thingsboardApiDashboard from '../api/dashboard.service';
25 25 import thingsboardApiCustomer from '../api/customer.service';
26 26 import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
  27 +import thingsboardDeviceFilter from '../components/device-filter.directive';
27 28 import thingsboardWidgetConfig from '../components/widget-config.directive';
28 29 import thingsboardDashboard from '../components/dashboard.directive';
29 30 import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
... ... @@ -36,12 +37,14 @@ import DashboardRoutes from './dashboard.routes';
36 37 import DashboardsController from './dashboards.controller';
37 38 import DashboardController from './dashboard.controller';
38 39 import DeviceAliasesController from './device-aliases.controller';
  40 +import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
39 41 import DashboardSettingsController from './dashboard-settings.controller';
40 42 import AssignDashboardToCustomerController from './assign-to-customer.controller';
41 43 import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
42 44 import AddWidgetController from './add-widget.controller';
43 45 import DashboardDirective from './dashboard.directive';
44 46 import EditWidgetDirective from './edit-widget.directive';
  47 +import AliasesDeviceSelectDirective from './aliases-device-select.directive';
45 48
46 49 export default angular.module('thingsboard.dashboard', [
47 50 uiRouter,
... ... @@ -55,6 +58,7 @@ export default angular.module('thingsboard.dashboard', [
55 58 thingsboardApiDashboard,
56 59 thingsboardApiCustomer,
57 60 thingsboardDetailsSidenav,
  61 + thingsboardDeviceFilter,
58 62 thingsboardWidgetConfig,
59 63 thingsboardDashboard,
60 64 thingsboardExpandFullscreen,
... ... @@ -64,10 +68,12 @@ export default angular.module('thingsboard.dashboard', [
64 68 .controller('DashboardsController', DashboardsController)
65 69 .controller('DashboardController', DashboardController)
66 70 .controller('DeviceAliasesController', DeviceAliasesController)
  71 + .controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)
67 72 .controller('DashboardSettingsController', DashboardSettingsController)
68 73 .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
69 74 .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
70 75 .controller('AddWidgetController', AddWidgetController)
71 76 .directive('tbDashboardDetails', DashboardDirective)
72 77 .directive('tbEditWidget', EditWidgetDirective)
  78 + .directive('tbAliasesDeviceSelect', AliasesDeviceSelectDirective)
73 79 .name;
... ...
... ... @@ -45,9 +45,13 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
45 45 };
46 46 aliasesInfo.datasourceAliases[0] = {
47 47 aliasName: deviceName,
48   - deviceId: deviceId
  48 + deviceFilter: {
  49 + useFilter: false,
  50 + deviceNameFilter: '',
  51 + deviceList: [deviceId]
  52 + }
49 53 };
50   - theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, 48, -1, -1);
  54 + theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, null, 48, -1, -1);
51 55 dashboardService.saveDashboard(theDashboard).then(
52 56 function success(dashboard) {
53 57 $mdDialog.hide();
... ...
... ... @@ -255,8 +255,16 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
255 255 scope.firstBundle = true;
256 256 scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
257 257
258   - scope.deviceAliases = {};
259   - scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId};
  258 + scope.aliasesInfo = {
  259 + deviceAliases: {
  260 + '1': {alias: scope.deviceName, deviceId: scope.deviceId}
  261 + },
  262 + deviceAliasesInfo: {
  263 + '1': [
  264 + {name: scope.deviceName, id: scope.deviceId}
  265 + ]
  266 + }
  267 + };
260 268
261 269 var dataKeyType = scope.attributeScope === types.latestTelemetry ?
262 270 types.dataKeyType.timeseries : types.dataKeyType.attribute;
... ...
... ... @@ -156,7 +156,7 @@
156 156 rn-swipe-disabled="true">
157 157 <li ng-repeat="widgets in widgetsList">
158 158 <tb-dashboard
159   - device-alias-list="deviceAliases"
  159 + aliases-info="aliasesInfo"
160 160 widgets="widgets"
161 161 get-st-diff="getServerTimeDiff()"
162 162 columns="20"
... ...
... ... @@ -41,6 +41,7 @@
41 41 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
42 42 <tb-attribute-table flex
43 43 device-id="vm.grid.operatingItem().id.id"
  44 + device-name="vm.grid.operatingItem().name"
44 45 default-attribute-scope="{{vm.types.latestTelemetry.value}}"
45 46 disable-attribute-scope-selection="true">
46 47 </tb-attribute-table>
... ...
... ... @@ -45,7 +45,25 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
45 45 exportToPc(prepareExport(widgetItem), name + '.json');
46 46 }
47 47
48   - function importWidget($event, dashboard) {
  48 + function prepareDeviceAlias(aliasInfo) {
  49 + var deviceFilter;
  50 + if (aliasInfo.deviceId) {
  51 + deviceFilter = {
  52 + useFilter: false,
  53 + deviceNameFilter: '',
  54 + deviceList: [aliasInfo.deviceId]
  55 + }
  56 + delete aliasInfo.deviceId;
  57 + } else {
  58 + deviceFilter = aliasInfo.deviceFilter;
  59 + }
  60 + return {
  61 + alias: aliasInfo.aliasName,
  62 + deviceFilter: deviceFilter
  63 + };
  64 + }
  65 +
  66 + function importWidget($event, dashboard, onAliasesUpdate) {
49 67 openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
50 68 function success(widgetItem) {
51 69 if (!validateImportedWidget(widgetItem)) {
... ... @@ -66,20 +84,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
66 84 if (datasourceAliases) {
67 85 for (datasourceIndex in datasourceAliases) {
68 86 datasourceAliasesMap[aliasId] = datasourceIndex;
69   - deviceAliases[aliasId] = {
70   - alias: datasourceAliases[datasourceIndex].aliasName,
71   - deviceId: datasourceAliases[datasourceIndex].deviceId
72   - };
  87 + deviceAliases[aliasId] = prepareDeviceAlias(datasourceAliases[datasourceIndex]);
73 88 aliasId++;
74 89 }
75 90 }
76 91 if (targetDeviceAliases) {
77 92 for (datasourceIndex in targetDeviceAliases) {
78 93 targetDeviceAliasesMap[aliasId] = datasourceIndex;
79   - deviceAliases[aliasId] = {
80   - alias: targetDeviceAliases[datasourceIndex].aliasName,
81   - deviceId: targetDeviceAliases[datasourceIndex].deviceId
82   - };
  94 + deviceAliases[aliasId] = prepareDeviceAlias(targetDeviceAliases[datasourceIndex]);
83 95 aliasId++;
84 96 }
85 97 }
... ... @@ -97,26 +109,26 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
97 109 var datasourceIndex;
98 110 if (datasourceAliasesMap[aliasId]) {
99 111 datasourceIndex = datasourceAliasesMap[aliasId];
100   - datasourceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
  112 + datasourceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
101 113 } else if (targetDeviceAliasesMap[aliasId]) {
102 114 datasourceIndex = targetDeviceAliasesMap[aliasId];
103   - targetDeviceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
  115 + targetDeviceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
104 116 }
105 117 }
106   - addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  118 + addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
107 119 },
108 120 function fail() {}
109 121 );
110 122 } else {
111   - addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  123 + addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
112 124 }
113 125 }
114 126 );
115 127 } else {
116   - addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  128 + addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
117 129 }
118 130 } else {
119   - addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  131 + addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
120 132 }
121 133 }
122 134 },
... ... @@ -140,8 +152,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
140 152 return true;
141 153 }
142 154
143   - function addImportedWidget(dashboard, widget, aliasesInfo, originalColumns) {
144   - itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, -1, -1);
  155 + function addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns) {
  156 + itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, -1, -1);
145 157 }
146 158
147 159 // Dashboard functions
... ... @@ -248,19 +260,18 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
248 260 function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
249 261 var aliasId = aliasIds[index];
250 262 var deviceAlias = deviceAliases[aliasId];
251   - if (deviceAlias.deviceId) {
252   - deviceService.getDevice(deviceAlias.deviceId, true).then(
253   - function success() {
  263 + deviceService.checkDeviceAlias(deviceAlias).then(
  264 + function(result) {
  265 + if (result) {
254 266 checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
255   - },
256   - function fail() {
  267 + } else {
257 268 var missingDeviceAlias = angular.copy(deviceAlias);
258   - missingDeviceAlias.deviceId = null;
  269 + missingDeviceAlias.deviceFilter = null;
259 270 missingDeviceAliases[aliasId] = missingDeviceAlias;
260 271 checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
261 272 }
262   - );
263   - }
  273 + }
  274 + );
264 275 }
265 276
266 277 function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
... ... @@ -274,7 +285,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
274 285 deviceAliases: missingDeviceAliases,
275 286 widgets: widgets,
276 287 isSingleWidget: isSingleWidget,
277   - isSingleDevice: false,
  288 + isSingleDeviceAlias: false,
278 289 singleDeviceAlias: null,
279 290 customTitle: customTitle,
280 291 disableAdd: true
... ...
... ... @@ -254,14 +254,19 @@ export default angular.module('thingsboard.locale', [])
254 254 "create-new-dashboard": "Create new dashboard",
255 255 "dashboard-file": "Dashboard file",
256 256 "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.",
257   - "dashboard-import-missing-aliases-title": "Select missing devices for dashboard aliases",
  257 + "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard",
258 258 "create-new-widget": "Create new widget",
259 259 "import-widget": "Import widget",
260 260 "widget-file": "Widget file",
261 261 "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.",
262   - "widget-import-missing-aliases-title": "Select missing devices used by widget",
  262 + "widget-import-missing-aliases-title": "Configure aliases used by imported widget",
263 263 "open-toolbar": "Open dashboard toolbar",
264   - "close-toolbar": "Close toolbar"
  264 + "close-toolbar": "Close toolbar",
  265 + "configuration-error": "Configuration error",
  266 + "alias-resolution-error-title": "Dashboard aliases configuration error",
  267 + "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.<br/>" +
  268 + "Please contact your administrator in order to resolve this issue.",
  269 + "select-devices": "Select devices"
265 270 },
266 271 "datakey": {
267 272 "settings": "Settings",
... ... @@ -301,12 +306,18 @@ export default angular.module('thingsboard.locale', [])
301 306 "create-new-alias": "Create a new one!",
302 307 "create-new-key": "Create a new one!",
303 308 "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.",
304   - "select-device-for-alias": "Select device for '{{alias}}' alias",
  309 + "configure-alias": "Configure '{{alias}}' alias",
305 310 "no-devices-matching": "No devices matching '{{device}}' were found.",
306 311 "alias": "Alias",
307 312 "alias-required": "Device alias is required.",
308 313 "remove-alias": "Remove device alias",
309 314 "add-alias": "Add device alias",
  315 + "name-starts-with": "Name starts with",
  316 + "device-list": "Device list",
  317 + "use-device-name-filter": "Use filter",
  318 + "device-list-empty": "No devices selected.",
  319 + "device-name-filter-required": "Device name filter is required.",
  320 + "device-name-filter-no-device-matched": "No devices starting with '{{device}}' were found.",
310 321 "add": "Add Device",
311 322 "assign-to-customer": "Assign to customer",
312 323 "assign-device-to-customer": "Assign Device(s) To Customer",
... ...
... ... @@ -43,19 +43,38 @@ function ItemBuffer(bufferStore, types) {
43 43 datasourceAliases: {
44 44 datasourceIndex: {
45 45 aliasName: "...",
46   - deviceId: "..."
  46 + deviceFilter: "..."
47 47 }
48 48 }
49 49 targetDeviceAliases: {
50 50 targetDeviceAliasIndex: {
51 51 aliasName: "...",
52   - deviceId: "..."
  52 + deviceFilter: "..."
53 53 }
54 54 }
55 55 ....
56 56 }
57 57 **/
58 58
  59 + function getDeviceFilter(alias) {
  60 + if (alias.deviceId) {
  61 + return {
  62 + useFilter: false,
  63 + deviceNameFilter: '',
  64 + deviceList: [alias.deviceId]
  65 + };
  66 + } else {
  67 + return alias.deviceFilter;
  68 + }
  69 + }
  70 +
  71 + function prepareAliasInfo(deviceAlias) {
  72 + return {
  73 + aliasName: deviceAlias.alias,
  74 + deviceFilter: getDeviceFilter(deviceAlias)
  75 + };
  76 + }
  77 +
59 78 function prepareWidgetItem(dashboard, widget) {
60 79 var aliasesInfo = {
61 80 datasourceAliases: {},
... ... @@ -75,10 +94,7 @@ function ItemBuffer(bufferStore, types) {
75 94 if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
76 95 deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
77 96 if (deviceAlias) {
78   - aliasesInfo.datasourceAliases[i] = {
79   - aliasName: deviceAlias.alias,
80   - deviceId: deviceAlias.deviceId
81   - }
  97 + aliasesInfo.datasourceAliases[i] = prepareAliasInfo(deviceAlias);
82 98 }
83 99 }
84 100 }
... ... @@ -89,10 +105,7 @@ function ItemBuffer(bufferStore, types) {
89 105 if (targetDeviceAliasId) {
90 106 deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
91 107 if (deviceAlias) {
92   - aliasesInfo.targetDeviceAliases[i] = {
93   - aliasName: deviceAlias.alias,
94   - deviceId: deviceAlias.deviceId
95   - }
  108 + aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(deviceAlias);
96 109 }
97 110 }
98 111 }
... ... @@ -114,7 +127,7 @@ function ItemBuffer(bufferStore, types) {
114 127 return bufferStore.get(WIDGET_ITEM);
115 128 }
116 129
117   - function pasteWidget(targetDashboard, position) {
  130 + function pasteWidget(targetDashboard, position, onAliasesUpdate) {
118 131 var widgetItemJson = bufferStore.get(WIDGET_ITEM);
119 132 if (widgetItemJson) {
120 133 var widgetItem = angular.fromJson(widgetItemJson);
... ... @@ -127,11 +140,11 @@ function ItemBuffer(bufferStore, types) {
127 140 targetRow = position.row;
128 141 targetColumn = position.column;
129 142 }
130   - addWidgetToDashboard(targetDashboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
  143 + addWidgetToDashboard(targetDashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, targetRow, targetColumn);
131 144 }
132 145 }
133 146
134   - function addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, row, column) {
  147 + function addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, row, column) {
135 148 var theDashboard;
136 149 if (dashboard) {
137 150 theDashboard = dashboard;
... ... @@ -144,7 +157,7 @@ function ItemBuffer(bufferStore, types) {
144 157 if (!theDashboard.configuration.deviceAliases) {
145 158 theDashboard.configuration.deviceAliases = {};
146 159 }
147   - updateAliases(theDashboard, widget, aliasesInfo);
  160 + var newDeviceAliases = updateAliases(theDashboard, widget, aliasesInfo);
148 161
149 162 if (!theDashboard.configuration.widgets) {
150 163 theDashboard.configuration.widgets = [];
... ... @@ -174,12 +187,19 @@ function ItemBuffer(bufferStore, types) {
174 187 widget.row = row;
175 188 widget.col = 0;
176 189 }
  190 + var aliasesUpdated = !angular.equals(newDeviceAliases, theDashboard.configuration.deviceAliases);
  191 + if (aliasesUpdated) {
  192 + theDashboard.configuration.deviceAliases = newDeviceAliases;
  193 + if (onAliasesUpdate) {
  194 + onAliasesUpdate();
  195 + }
  196 + }
177 197 theDashboard.configuration.widgets.push(widget);
178 198 return theDashboard;
179 199 }
180 200
181 201 function updateAliases(dashboard, widget, aliasesInfo) {
182   - var deviceAliases = dashboard.configuration.deviceAliases;
  202 + var deviceAliases = angular.copy(dashboard.configuration.deviceAliases);
183 203 var aliasInfo;
184 204 var newAliasId;
185 205 for (var datasourceIndex in aliasesInfo.datasourceAliases) {
... ... @@ -192,12 +212,19 @@ function ItemBuffer(bufferStore, types) {
192 212 newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
193 213 widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
194 214 }
  215 + return deviceAliases;
  216 + }
  217 +
  218 + function isDeviceFiltersEqual(alias1, alias2) {
  219 + var filter1 = getDeviceFilter(alias1);
  220 + var filter2 = getDeviceFilter(alias2);
  221 + return angular.equals(filter1, filter2);
195 222 }
196 223
197 224 function getDeviceAliasId(deviceAliases, aliasInfo) {
198 225 var newAliasId;
199 226 for (var aliasId in deviceAliases) {
200   - if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) {
  227 + if (isDeviceFiltersEqual(deviceAliases[aliasId], aliasInfo)) {
201 228 newAliasId = aliasId;
202 229 break;
203 230 }
... ... @@ -209,7 +236,7 @@ function ItemBuffer(bufferStore, types) {
209 236 newAliasId = Math.max(newAliasId, aliasId);
210 237 }
211 238 newAliasId++;
212   - deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId};
  239 + deviceAliases[newAliasId] = {alias: newAliasName, deviceFilter: aliasInfo.deviceFilter};
213 240 }
214 241 return newAliasId;
215 242 }
... ...
... ... @@ -35,7 +35,6 @@ export default class TbFlot {
35 35 var colors = [];
36 36 for (var i in ctx.data) {
37 37 var series = ctx.data[i];
38   - series.label = series.dataKey.label;
39 38 colors.push(series.dataKey.color);
40 39 var keySettings = series.dataKey.settings;
41 40
... ... @@ -130,7 +129,7 @@ export default class TbFlot {
130 129
131 130 if (this.chartType === 'pie') {
132 131 ctx.tooltipFormatter = function(item) {
133   - var divElement = seriesInfoDiv(item.series.label, item.series.color,
  132 + var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
134 133 item.datapoint[1][0][1], tbFlot.ctx.trackUnits, tbFlot.ctx.trackDecimals, true, item.series.percent);
135 134 return divElement.prop('outerHTML');
136 135 };
... ... @@ -313,7 +312,7 @@ export default class TbFlot {
313 312
314 313 if (options.series.pie.label.show) {
315 314 options.series.pie.label.formatter = function (label, series) {
316   - return "<div class='pie-label'>" + label + "<br/>" + Math.round(series.percent) + "%</div>";
  315 + return "<div class='pie-label'>" + series.dataKey.label + "<br/>" + Math.round(series.percent) + "%</div>";
317 316 }
318 317 options.series.pie.label.radius = 3/4;
319 318 options.series.pie.label.background = {
... ... @@ -346,7 +345,7 @@ export default class TbFlot {
346 345 }
347 346
348 347 update() {
349   - if (!this.isMouseInteraction) {
  348 + if (!this.isMouseInteraction && this.ctx.plot) {
350 349 if (this.chartType === 'line' || this.chartType === 'bar') {
351 350 this.options.xaxis.min = this.ctx.timeWindow.minTime;
352 351 this.options.xaxis.max = this.ctx.timeWindow.maxTime;
... ... @@ -918,7 +917,7 @@ export default class TbFlot {
918 917 value: value,
919 918 hoverIndex: hoverIndex,
920 919 color: series.dataKey.color,
921   - label: series.label,
  920 + label: series.dataKey.label,
922 921 time: pointTime,
923 922 distance: hoverDistance,
924 923 index: i
... ...