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