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