Commit 9c44920fe749b07fdb089a6a9eb1075789fc53ed

Authored by Viacheslav Kukhtyn
2 parents 9829dd17 6814c21e

Merge branch 'master' into feature/log-telemetry-updated

Showing 34 changed files with 810 additions and 255 deletions
@@ -16,18 +16,23 @@ @@ -16,18 +16,23 @@
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
18 import org.springframework.beans.factory.annotation.Autowired; 18 import org.springframework.beans.factory.annotation.Autowired;
  19 +import org.springframework.http.ResponseEntity;
19 import org.springframework.security.access.prepost.PreAuthorize; 20 import org.springframework.security.access.prepost.PreAuthorize;
20 import org.springframework.web.bind.annotation.RequestBody; 21 import org.springframework.web.bind.annotation.RequestBody;
21 import org.springframework.web.bind.annotation.RequestMapping; 22 import org.springframework.web.bind.annotation.RequestMapping;
22 import org.springframework.web.bind.annotation.RequestMethod; 23 import org.springframework.web.bind.annotation.RequestMethod;
  24 +import org.springframework.web.bind.annotation.RequestParam;
23 import org.springframework.web.bind.annotation.ResponseBody; 25 import org.springframework.web.bind.annotation.ResponseBody;
24 import org.springframework.web.bind.annotation.RestController; 26 import org.springframework.web.bind.annotation.RestController;
  27 +import org.springframework.web.context.request.async.DeferredResult;
25 import org.thingsboard.server.common.data.exception.ThingsboardException; 28 import org.thingsboard.server.common.data.exception.ThingsboardException;
  29 +import org.thingsboard.server.common.data.id.TenantId;
26 import org.thingsboard.server.common.data.page.PageData; 30 import org.thingsboard.server.common.data.page.PageData;
27 import org.thingsboard.server.common.data.query.AlarmData; 31 import org.thingsboard.server.common.data.query.AlarmData;
28 import org.thingsboard.server.common.data.query.AlarmDataQuery; 32 import org.thingsboard.server.common.data.query.AlarmDataQuery;
29 import org.thingsboard.server.common.data.query.EntityCountQuery; 33 import org.thingsboard.server.common.data.query.EntityCountQuery;
30 import org.thingsboard.server.common.data.query.EntityData; 34 import org.thingsboard.server.common.data.query.EntityData;
  35 +import org.thingsboard.server.common.data.query.EntityDataPageLink;
31 import org.thingsboard.server.common.data.query.EntityDataQuery; 36 import org.thingsboard.server.common.data.query.EntityDataQuery;
32 import org.thingsboard.server.queue.util.TbCoreComponent; 37 import org.thingsboard.server.queue.util.TbCoreComponent;
33 import org.thingsboard.server.service.query.EntityQueryService; 38 import org.thingsboard.server.service.query.EntityQueryService;
@@ -40,6 +45,7 @@ public class EntityQueryController extends BaseController { @@ -40,6 +45,7 @@ public class EntityQueryController extends BaseController {
40 @Autowired 45 @Autowired
41 private EntityQueryService entityQueryService; 46 private EntityQueryService entityQueryService;
42 47
  48 + private static final int MAX_PAGE_SIZE = 100;
43 49
44 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 50 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
45 @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST) 51 @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST)
@@ -76,4 +82,24 @@ public class EntityQueryController extends BaseController { @@ -76,4 +82,24 @@ public class EntityQueryController extends BaseController {
76 throw handleException(e); 82 throw handleException(e);
77 } 83 }
78 } 84 }
  85 +
  86 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
  87 + @RequestMapping(value = "/entitiesQuery/find/keys", method = RequestMethod.POST)
  88 + @ResponseBody
  89 + public DeferredResult<ResponseEntity> findEntityTimeseriesAndAttributesKeysByQuery(@RequestBody EntityDataQuery query,
  90 + @RequestParam("timeseries") boolean isTimeseries,
  91 + @RequestParam("attributes") boolean isAttributes) throws ThingsboardException {
  92 + TenantId tenantId = getTenantId();
  93 + checkNotNull(query);
  94 + try {
  95 + EntityDataPageLink pageLink = query.getPageLink();
  96 + if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
  97 + pageLink.setPageSize(MAX_PAGE_SIZE);
  98 + }
  99 + return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes);
  100 + } catch (Exception e) {
  101 + throw handleException(e);
  102 + }
  103 + }
  104 +
79 } 105 }
@@ -15,11 +15,23 @@ @@ -15,11 +15,23 @@
15 */ 15 */
16 package org.thingsboard.server.service.query; 16 package org.thingsboard.server.service.query;
17 17
  18 +import com.fasterxml.jackson.databind.node.ArrayNode;
  19 +import com.fasterxml.jackson.databind.node.ObjectNode;
  20 +import com.google.common.util.concurrent.FutureCallback;
  21 +import com.google.common.util.concurrent.Futures;
  22 +import com.google.common.util.concurrent.ListenableFuture;
18 import lombok.extern.slf4j.Slf4j; 23 import lombok.extern.slf4j.Slf4j;
  24 +import org.checkerframework.checker.nullness.qual.Nullable;
19 import org.springframework.beans.factory.annotation.Autowired; 25 import org.springframework.beans.factory.annotation.Autowired;
20 import org.springframework.beans.factory.annotation.Value; 26 import org.springframework.beans.factory.annotation.Value;
  27 +import org.springframework.http.HttpStatus;
  28 +import org.springframework.http.ResponseEntity;
21 import org.springframework.stereotype.Service; 29 import org.springframework.stereotype.Service;
  30 +import org.springframework.util.CollectionUtils;
  31 +import org.springframework.web.context.request.async.DeferredResult;
  32 +import org.thingsboard.server.common.data.EntityType;
22 import org.thingsboard.server.common.data.id.EntityId; 33 import org.thingsboard.server.common.data.id.EntityId;
  34 +import org.thingsboard.server.common.data.id.TenantId;
23 import org.thingsboard.server.common.data.page.PageData; 35 import org.thingsboard.server.common.data.page.PageData;
24 import org.thingsboard.server.common.data.query.AlarmData; 36 import org.thingsboard.server.common.data.query.AlarmData;
25 import org.thingsboard.server.common.data.query.AlarmDataQuery; 37 import org.thingsboard.server.common.data.query.AlarmDataQuery;
@@ -31,12 +43,25 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -31,12 +43,25 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder;
31 import org.thingsboard.server.common.data.query.EntityKey; 43 import org.thingsboard.server.common.data.query.EntityKey;
32 import org.thingsboard.server.common.data.query.EntityKeyType; 44 import org.thingsboard.server.common.data.query.EntityKeyType;
33 import org.thingsboard.server.dao.alarm.AlarmService; 45 import org.thingsboard.server.dao.alarm.AlarmService;
  46 +import org.thingsboard.server.dao.attributes.AttributesService;
34 import org.thingsboard.server.dao.entity.EntityService; 47 import org.thingsboard.server.dao.entity.EntityService;
35 import org.thingsboard.server.dao.model.ModelConstants; 48 import org.thingsboard.server.dao.model.ModelConstants;
  49 +import org.thingsboard.server.dao.timeseries.TimeseriesService;
  50 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
36 import org.thingsboard.server.queue.util.TbCoreComponent; 51 import org.thingsboard.server.queue.util.TbCoreComponent;
  52 +import org.thingsboard.server.service.executors.DbCallbackExecutorService;
  53 +import org.thingsboard.server.service.security.AccessValidator;
37 import org.thingsboard.server.service.security.model.SecurityUser; 54 import org.thingsboard.server.service.security.model.SecurityUser;
38 55
  56 +import java.util.ArrayList;
  57 +import java.util.Collection;
  58 +import java.util.Collections;
39 import java.util.LinkedHashMap; 59 import java.util.LinkedHashMap;
  60 +import java.util.List;
  61 +import java.util.Map;
  62 +import java.util.Set;
  63 +import java.util.function.Consumer;
  64 +import java.util.stream.Collectors;
40 65
41 @Service 66 @Service
42 @Slf4j 67 @Slf4j
@@ -52,6 +77,15 @@ public class DefaultEntityQueryService implements EntityQueryService { @@ -52,6 +77,15 @@ public class DefaultEntityQueryService implements EntityQueryService {
52 @Value("${server.ws.max_entities_per_alarm_subscription:1000}") 77 @Value("${server.ws.max_entities_per_alarm_subscription:1000}")
53 private int maxEntitiesPerAlarmSubscription; 78 private int maxEntitiesPerAlarmSubscription;
54 79
  80 + @Autowired
  81 + private DbCallbackExecutorService dbCallbackExecutor;
  82 +
  83 + @Autowired
  84 + private TimeseriesService timeseriesService;
  85 +
  86 + @Autowired
  87 + private AttributesService attributesService;
  88 +
55 @Override 89 @Override
56 public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) { 90 public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) {
57 return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); 91 return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query);
@@ -100,4 +134,103 @@ public class DefaultEntityQueryService implements EntityQueryService { @@ -100,4 +134,103 @@ public class DefaultEntityQueryService implements EntityQueryService {
100 EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); 134 EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
101 return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); 135 return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
102 } 136 }
  137 +
  138 + @Override
  139 + public DeferredResult<ResponseEntity> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
  140 + boolean isTimeseries, boolean isAttributes) {
  141 + final DeferredResult<ResponseEntity> response = new DeferredResult<>();
  142 + if (!isAttributes && !isTimeseries) {
  143 + replyWithEmptyResponse(response);
  144 + return response;
  145 + }
  146 +
  147 + List<EntityId> ids = this.findEntityDataByQuery(securityUser, query).getData().stream()
  148 + .map(EntityData::getEntityId)
  149 + .collect(Collectors.toList());
  150 + if (ids.isEmpty()) {
  151 + replyWithEmptyResponse(response);
  152 + return response;
  153 + }
  154 +
  155 + Set<EntityType> types = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet());
  156 + final ListenableFuture<List<String>> timeseriesKeysFuture;
  157 + final ListenableFuture<List<String>> attributesKeysFuture;
  158 +
  159 + if (isTimeseries) {
  160 + timeseriesKeysFuture = dbCallbackExecutor.submit(() -> timeseriesService.findAllKeysByEntityIds(tenantId, ids));
  161 + } else {
  162 + timeseriesKeysFuture = null;
  163 + }
  164 +
  165 + if (isAttributes) {
  166 + Map<EntityType, List<EntityId>> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType));
  167 + List<ListenableFuture<List<String>>> futures = new ArrayList<>(typesMap.size());
  168 + typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, type, entityIds))));
  169 + attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> {
  170 + if (CollectionUtils.isEmpty(lists)) {
  171 + return Collections.emptyList();
  172 + }
  173 + return lists.stream().flatMap(List::stream).distinct().sorted().collect(Collectors.toList());
  174 + }, dbCallbackExecutor);
  175 + } else {
  176 + attributesKeysFuture = null;
  177 + }
  178 +
  179 + if (isTimeseries && isAttributes) {
  180 + Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture).run(() -> {
  181 + try {
  182 + replyWithResponse(response, types, timeseriesKeysFuture.get(), attributesKeysFuture.get());
  183 + } catch (Exception e) {
  184 + log.error("Failed to fetch timeseries and attributes keys!", e);
  185 + AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
  186 + }
  187 + }, dbCallbackExecutor);
  188 + } else if (isTimeseries) {
  189 + addCallback(timeseriesKeysFuture, keys -> replyWithResponse(response, types, keys, null),
  190 + error -> {
  191 + log.error("Failed to fetch timeseries keys!", error);
  192 + AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR);
  193 + });
  194 + } else {
  195 + addCallback(attributesKeysFuture, keys -> replyWithResponse(response, types, null, keys),
  196 + error -> {
  197 + log.error("Failed to fetch attributes keys!", error);
  198 + AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR);
  199 + });
  200 + }
  201 + return response;
  202 + }
  203 +
  204 + private void replyWithResponse(DeferredResult<ResponseEntity> response, Set<EntityType> types, List<String> timeseriesKeys, List<String> attributesKeys) {
  205 + ObjectNode json = JacksonUtil.newObjectNode();
  206 + addItemsToArrayNode(json.putArray("entityTypes"), types);
  207 + addItemsToArrayNode(json.putArray("timeseries"), timeseriesKeys);
  208 + addItemsToArrayNode(json.putArray("attribute"), attributesKeys);
  209 + response.setResult(new ResponseEntity(json, HttpStatus.OK));
  210 + }
  211 +
  212 + private void replyWithEmptyResponse(DeferredResult<ResponseEntity> response) {
  213 + replyWithResponse(response, Collections.emptySet(), Collections.emptyList(), Collections.emptyList());
  214 + }
  215 +
  216 + private void addItemsToArrayNode(ArrayNode arrayNode, Collection<?> collection) {
  217 + if (!CollectionUtils.isEmpty(collection)) {
  218 + collection.forEach(item -> arrayNode.add(item.toString()));
  219 + }
  220 + }
  221 +
  222 + private void addCallback(ListenableFuture<List<String>> future, Consumer<List<String>> success, Consumer<Throwable> error) {
  223 + Futures.addCallback(future, new FutureCallback<List<String>>() {
  224 + @Override
  225 + public void onSuccess(@Nullable List<String> keys) {
  226 + success.accept(keys);
  227 + }
  228 +
  229 + @Override
  230 + public void onFailure(Throwable t) {
  231 + error.accept(t);
  232 + }
  233 + }, dbCallbackExecutor);
  234 + }
  235 +
103 } 236 }
@@ -15,6 +15,9 @@ @@ -15,6 +15,9 @@
15 */ 15 */
16 package org.thingsboard.server.service.query; 16 package org.thingsboard.server.service.query;
17 17
  18 +import org.springframework.http.ResponseEntity;
  19 +import org.springframework.web.context.request.async.DeferredResult;
  20 +import org.thingsboard.server.common.data.id.TenantId;
18 import org.thingsboard.server.common.data.page.PageData; 21 import org.thingsboard.server.common.data.page.PageData;
19 import org.thingsboard.server.common.data.query.AlarmData; 22 import org.thingsboard.server.common.data.query.AlarmData;
20 import org.thingsboard.server.common.data.query.AlarmDataQuery; 23 import org.thingsboard.server.common.data.query.AlarmDataQuery;
@@ -31,4 +34,7 @@ public interface EntityQueryService { @@ -31,4 +34,7 @@ public interface EntityQueryService {
31 34
32 PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query); 35 PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query);
33 36
  37 + DeferredResult<ResponseEntity> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
  38 + boolean isTimeseries, boolean isAttributes);
  39 +
34 } 40 }
@@ -192,8 +192,9 @@ cassandra: @@ -192,8 +192,9 @@ cassandra:
192 read_consistency_level: "${CASSANDRA_READ_CONSISTENCY_LEVEL:ONE}" 192 read_consistency_level: "${CASSANDRA_READ_CONSISTENCY_LEVEL:ONE}"
193 write_consistency_level: "${CASSANDRA_WRITE_CONSISTENCY_LEVEL:ONE}" 193 write_consistency_level: "${CASSANDRA_WRITE_CONSISTENCY_LEVEL:ONE}"
194 default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}" 194 default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
195 - # Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS,INDEFINITE 195 + # Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
196 ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" 196 ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
  197 + ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
197 ts_key_value_ttl: "${TS_KV_TTL:0}" 198 ts_key_value_ttl: "${TS_KV_TTL:0}"
198 events_ttl: "${TS_EVENTS_TTL:0}" 199 events_ttl: "${TS_EVENTS_TTL:0}"
199 # Specify TTL of debug log in seconds. The current value corresponds to one week 200 # Specify TTL of debug log in seconds. The current value corresponds to one week
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 package org.thingsboard.server.dao.attributes; 16 package org.thingsboard.server.dao.attributes;
17 17
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import org.thingsboard.server.common.data.EntityType;
19 import org.thingsboard.server.common.data.id.DeviceProfileId; 20 import org.thingsboard.server.common.data.id.DeviceProfileId;
20 import org.thingsboard.server.common.data.id.EntityId; 21 import org.thingsboard.server.common.data.id.EntityId;
21 import org.thingsboard.server.common.data.id.TenantId; 22 import org.thingsboard.server.common.data.id.TenantId;
@@ -42,4 +43,6 @@ public interface AttributesService { @@ -42,4 +43,6 @@ public interface AttributesService {
42 43
43 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); 44 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
44 45
  46 + List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds);
  47 +
45 } 48 }
@@ -50,4 +50,6 @@ public interface TimeseriesService { @@ -50,4 +50,6 @@ public interface TimeseriesService {
50 ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId); 50 ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId);
51 51
52 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); 52 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
  53 +
  54 + List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
53 } 55 }
@@ -462,26 +462,26 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -462,26 +462,26 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
462 String clientId = msg.payload().clientIdentifier(); 462 String clientId = msg.payload().clientIdentifier();
463 if (DataConstants.PROVISION.equals(userName) || DataConstants.PROVISION.equals(clientId)) { 463 if (DataConstants.PROVISION.equals(userName) || DataConstants.PROVISION.equals(clientId)) {
464 deviceSessionCtx.setProvisionOnly(true); 464 deviceSessionCtx.setProvisionOnly(true);
465 - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); 465 + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, msg));
466 } else { 466 } else {
467 X509Certificate cert; 467 X509Certificate cert;
468 if (sslHandler != null && (cert = getX509Certificate()) != null) { 468 if (sslHandler != null && (cert = getX509Certificate()) != null) {
469 - processX509CertConnect(ctx, cert); 469 + processX509CertConnect(ctx, cert, msg);
470 } else { 470 } else {
471 processAuthTokenConnect(ctx, msg); 471 processAuthTokenConnect(ctx, msg);
472 } 472 }
473 } 473 }
474 } 474 }
475 475
476 - private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {  
477 - String userName = msg.payload().userName(); 476 + private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage connectMessage) {
  477 + String userName = connectMessage.payload().userName();
478 log.info("[{}] Processing connect msg for client with user name: {}!", sessionId, userName); 478 log.info("[{}] Processing connect msg for client with user name: {}!", sessionId, userName);
479 TransportProtos.ValidateBasicMqttCredRequestMsg.Builder request = TransportProtos.ValidateBasicMqttCredRequestMsg.newBuilder() 479 TransportProtos.ValidateBasicMqttCredRequestMsg.Builder request = TransportProtos.ValidateBasicMqttCredRequestMsg.newBuilder()
480 - .setClientId(msg.payload().clientIdentifier()); 480 + .setClientId(connectMessage.payload().clientIdentifier());
481 if (userName != null) { 481 if (userName != null) {
482 request.setUserName(userName); 482 request.setUserName(userName);
483 } 483 }
484 - String password = msg.payload().password(); 484 + String password = connectMessage.payload().password();
485 if (password != null) { 485 if (password != null) {
486 request.setPassword(password); 486 request.setPassword(password);
487 } 487 }
@@ -489,19 +489,19 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -489,19 +489,19 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
489 new TransportServiceCallback<ValidateDeviceCredentialsResponse>() { 489 new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
490 @Override 490 @Override
491 public void onSuccess(ValidateDeviceCredentialsResponse msg) { 491 public void onSuccess(ValidateDeviceCredentialsResponse msg) {
492 - onValidateDeviceResponse(msg, ctx); 492 + onValidateDeviceResponse(msg, ctx, connectMessage);
493 } 493 }
494 494
495 @Override 495 @Override
496 public void onError(Throwable e) { 496 public void onError(Throwable e) {
497 log.trace("[{}] Failed to process credentials: {}", address, userName, e); 497 log.trace("[{}] Failed to process credentials: {}", address, userName, e);
498 - ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); 498 + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage));
499 ctx.close(); 499 ctx.close();
500 } 500 }
501 }); 501 });
502 } 502 }
503 503
504 - private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) { 504 + private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert, MqttConnectMessage connectMessage) {
505 try { 505 try {
506 if (!context.isSkipValidityCheckForClientCert()) { 506 if (!context.isSkipValidityCheckForClientCert()) {
507 cert.checkValidity(); 507 cert.checkValidity();
@@ -512,18 +512,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -512,18 +512,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
512 new TransportServiceCallback<ValidateDeviceCredentialsResponse>() { 512 new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
513 @Override 513 @Override
514 public void onSuccess(ValidateDeviceCredentialsResponse msg) { 514 public void onSuccess(ValidateDeviceCredentialsResponse msg) {
515 - onValidateDeviceResponse(msg, ctx); 515 + onValidateDeviceResponse(msg, ctx, connectMessage);
516 } 516 }
517 517
518 @Override 518 @Override
519 public void onError(Throwable e) { 519 public void onError(Throwable e) {
520 log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e); 520 log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e);
521 - ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); 521 + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage));
522 ctx.close(); 522 ctx.close();
523 } 523 }
524 }); 524 });
525 } catch (Exception e) { 525 } catch (Exception e) {
526 - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED)); 526 + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED, connectMessage));
527 ctx.close(); 527 ctx.close();
528 } 528 }
529 } 529 }
@@ -547,11 +547,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -547,11 +547,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
547 doDisconnect(); 547 doDisconnect();
548 } 548 }
549 549
550 - private MqttConnAckMessage createMqttConnAckMsg(MqttConnectReturnCode returnCode) { 550 + private MqttConnAckMessage createMqttConnAckMsg(MqttConnectReturnCode returnCode, MqttConnectMessage msg) {
551 MqttFixedHeader mqttFixedHeader = 551 MqttFixedHeader mqttFixedHeader =
552 new MqttFixedHeader(CONNACK, false, AT_MOST_ONCE, false, 0); 552 new MqttFixedHeader(CONNACK, false, AT_MOST_ONCE, false, 0);
553 MqttConnAckVariableHeader mqttConnAckVariableHeader = 553 MqttConnAckVariableHeader mqttConnAckVariableHeader =
554 - new MqttConnAckVariableHeader(returnCode, true); 554 + new MqttConnAckVariableHeader(returnCode, !msg.variableHeader().isCleanSession());
555 return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader); 555 return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader);
556 } 556 }
557 557
@@ -627,9 +627,10 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -627,9 +627,10 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
627 } 627 }
628 } 628 }
629 629
630 - private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx) { 630 +
  631 + private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx, MqttConnectMessage connectMessage) {
631 if (!msg.hasDeviceInfo()) { 632 if (!msg.hasDeviceInfo()) {
632 - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED)); 633 + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED, connectMessage));
633 ctx.close(); 634 ctx.close();
634 } else { 635 } else {
635 deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo()); 636 deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo());
@@ -640,7 +641,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -640,7 +641,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
640 public void onSuccess(Void msg) { 641 public void onSuccess(Void msg) {
641 transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); 642 transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this);
642 checkGatewaySession(); 643 checkGatewaySession();
643 - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); 644 + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage));
644 log.info("[{}] Client connected!", sessionId); 645 log.info("[{}] Client connected!", sessionId);
645 } 646 }
646 647
@@ -651,7 +652,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -651,7 +652,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
651 } else { 652 } else {
652 log.warn("[{}] Failed to submit session event", sessionId, e); 653 log.warn("[{}] Failed to submit session event", sessionId, e);
653 } 654 }
654 - ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); 655 + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage));
655 ctx.close(); 656 ctx.close();
656 } 657 }
657 }); 658 });
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 package org.thingsboard.server.dao.attributes; 16 package org.thingsboard.server.dao.attributes;
17 17
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import org.thingsboard.server.common.data.EntityType;
19 import org.thingsboard.server.common.data.id.DeviceProfileId; 20 import org.thingsboard.server.common.data.id.DeviceProfileId;
20 import org.thingsboard.server.common.data.id.EntityId; 21 import org.thingsboard.server.common.data.id.EntityId;
21 import org.thingsboard.server.common.data.id.TenantId; 22 import org.thingsboard.server.common.data.id.TenantId;
@@ -41,4 +42,6 @@ public interface AttributesDao { @@ -41,4 +42,6 @@ public interface AttributesDao {
41 ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys); 42 ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys);
42 43
43 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); 44 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
  45 +
  46 + List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds);
44 } 47 }
@@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture; 20 import com.google.common.util.concurrent.ListenableFuture;
21 import org.springframework.beans.factory.annotation.Autowired; 21 import org.springframework.beans.factory.annotation.Autowired;
22 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
  23 +import org.thingsboard.server.common.data.EntityType;
23 import org.thingsboard.server.common.data.id.DeviceProfileId; 24 import org.thingsboard.server.common.data.id.DeviceProfileId;
24 import org.thingsboard.server.common.data.id.EntityId; 25 import org.thingsboard.server.common.data.id.EntityId;
25 import org.thingsboard.server.common.data.id.TenantId; 26 import org.thingsboard.server.common.data.id.TenantId;
@@ -66,6 +67,11 @@ public class BaseAttributesService implements AttributesService { @@ -66,6 +67,11 @@ public class BaseAttributesService implements AttributesService {
66 } 67 }
67 68
68 @Override 69 @Override
  70 + public List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds) {
  71 + return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
  72 + }
  73 +
  74 + @Override
69 public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) { 75 public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
70 validate(entityId, scope); 76 validate(entityId, scope);
71 attributes.forEach(attribute -> validate(attribute)); 77 attributes.forEach(attribute -> validate(attribute));
@@ -56,5 +56,8 @@ public interface AttributeKvRepository extends CrudRepository<AttributeKvEntity, @@ -56,5 +56,8 @@ public interface AttributeKvRepository extends CrudRepository<AttributeKvEntity,
56 "AND entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true) 56 "AND entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true)
57 List<String> findAllKeysByTenantId(@Param("tenantId") UUID tenantId); 57 List<String> findAllKeysByTenantId(@Param("tenantId") UUID tenantId);
58 58
  59 + @Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE entity_type = :entityType " +
  60 + "AND entity_id in :entityIds ORDER BY attribute_key", nativeQuery = true)
  61 + List<String> findAllKeysByEntityIds(@Param("entityType") String entityType, @Param("entityIds") List<UUID> entityIds);
59 } 62 }
60 63
@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
22 import org.springframework.beans.factory.annotation.Autowired; 22 import org.springframework.beans.factory.annotation.Autowired;
23 import org.springframework.beans.factory.annotation.Value; 23 import org.springframework.beans.factory.annotation.Value;
24 import org.springframework.stereotype.Component; 24 import org.springframework.stereotype.Component;
  25 +import org.thingsboard.server.common.data.EntityType;
25 import org.thingsboard.server.common.data.id.DeviceProfileId; 26 import org.thingsboard.server.common.data.id.DeviceProfileId;
26 import org.thingsboard.server.common.data.id.EntityId; 27 import org.thingsboard.server.common.data.id.EntityId;
27 import org.thingsboard.server.common.data.id.TenantId; 28 import org.thingsboard.server.common.data.id.TenantId;
@@ -146,6 +147,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @@ -146,6 +147,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
146 } 147 }
147 148
148 @Override 149 @Override
  150 + public List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds) {
  151 + return attributeKvRepository
  152 + .findAllKeysByEntityIds(entityType.name(), entityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
  153 + }
  154 +
  155 + @Override
149 public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) { 156 public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) {
150 AttributeKvEntity entity = new AttributeKvEntity(); 157 AttributeKvEntity entity = new AttributeKvEntity();
151 entity.setId(new AttributeKvCompositeKey(entityId.getEntityType(), entityId.getId(), attributeType, attribute.getKey())); 158 entity.setId(new AttributeKvCompositeKey(entityId.getEntityType(), entityId.getId(), attributeType, attribute.getKey()));
@@ -61,6 +61,7 @@ import java.util.Optional; @@ -61,6 +61,7 @@ import java.util.Optional;
61 import java.util.UUID; 61 import java.util.UUID;
62 import java.util.concurrent.ExecutionException; 62 import java.util.concurrent.ExecutionException;
63 import java.util.function.Function; 63 import java.util.function.Function;
  64 +import java.util.stream.Collectors;
64 65
65 @Slf4j 66 @Slf4j
66 @Component 67 @Component
@@ -169,6 +170,11 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @@ -169,6 +170,11 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
169 } 170 }
170 } 171 }
171 172
  173 + @Override
  174 + public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
  175 + return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
  176 + }
  177 +
172 private ListenableFuture<Void> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { 178 private ListenableFuture<Void> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
173 ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query); 179 ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query);
174 return Futures.transformAsync(future, entryList -> { 180 return Futures.transformAsync(future, entryList -> {
@@ -36,4 +36,9 @@ public interface TsKvLatestRepository extends CrudRepository<TsKvLatestEntity, T @@ -36,4 +36,9 @@ public interface TsKvLatestRepository extends CrudRepository<TsKvLatestEntity, T
36 "WHERE ts_kv_latest.entity_id IN (SELECT id FROM device WHERE tenant_id = :tenant_id limit 100) ORDER BY ts_kv_dictionary.key", nativeQuery = true) 36 "WHERE ts_kv_latest.entity_id IN (SELECT id FROM device WHERE tenant_id = :tenant_id limit 100) ORDER BY ts_kv_dictionary.key", nativeQuery = true)
37 List<String> getKeysByTenantId(@Param("tenant_id") UUID tenantId); 37 List<String> getKeysByTenantId(@Param("tenant_id") UUID tenantId);
38 38
  39 + @Query(value = "SELECT DISTINCT ts_kv_dictionary.key AS strKey FROM ts_kv_latest " +
  40 + "INNER JOIN ts_kv_dictionary ON ts_kv_latest.key = ts_kv_dictionary.key_id " +
  41 + "WHERE ts_kv_latest.entity_id IN :entityIds ORDER BY ts_kv_dictionary.key", nativeQuery = true)
  42 + List<String> findAllKeysByEntityIds(@Param("entityIds") List<UUID> entityIds);
  43 +
39 } 44 }
@@ -122,6 +122,11 @@ public class BaseTimeseriesService implements TimeseriesService { @@ -122,6 +122,11 @@ public class BaseTimeseriesService implements TimeseriesService {
122 } 122 }
123 123
124 @Override 124 @Override
  125 + public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
  126 + return timeseriesLatestDao.findAllKeysByEntityIds(tenantId, entityIds);
  127 + }
  128 +
  129 + @Override
125 public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { 130 public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
126 validate(entityId); 131 validate(entityId);
127 if (tsKvEntry == null) { 132 if (tsKvEntry == null) {
@@ -79,12 +79,17 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -79,12 +79,17 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
79 79
80 protected static List<Long> FIXED_PARTITION = Arrays.asList(new Long[]{0L}); 80 protected static List<Long> FIXED_PARTITION = Arrays.asList(new Long[]{0L});
81 81
  82 + private CassandraTsPartitionsCache cassandraTsPartitionsCache;
  83 +
82 @Autowired 84 @Autowired
83 private Environment environment; 85 private Environment environment;
84 86
85 @Value("${cassandra.query.ts_key_value_partitioning}") 87 @Value("${cassandra.query.ts_key_value_partitioning}")
86 private String partitioning; 88 private String partitioning;
87 89
  90 + @Value("${cassandra.query.ts_key_value_partitions_max_cache_size:100000}")
  91 + private long partitionsCacheSize;
  92 +
88 @Value("${cassandra.query.ts_key_value_ttl}") 93 @Value("${cassandra.query.ts_key_value_ttl}")
89 private long systemTtl; 94 private long systemTtl;
90 95
@@ -111,13 +116,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -111,13 +116,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
111 super.startExecutor(); 116 super.startExecutor();
112 if (!isInstall()) { 117 if (!isInstall()) {
113 getFetchStmt(Aggregation.NONE, DESC_ORDER); 118 getFetchStmt(Aggregation.NONE, DESC_ORDER);
114 - }  
115 - Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);  
116 - if (partition.isPresent()) {  
117 - tsFormat = partition.get();  
118 - } else {  
119 - log.warn("Incorrect configuration of partitioning {}", partitioning);  
120 - throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); 119 + Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);
  120 + if (partition.isPresent()) {
  121 + tsFormat = partition.get();
  122 + if (!isFixedPartitioning() && partitionsCacheSize > 0) {
  123 + cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize);
  124 + }
  125 + } else {
  126 + log.warn("Incorrect configuration of partitioning {}", partitioning);
  127 + throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
  128 + }
121 } 129 }
122 } 130 }
123 131
@@ -175,17 +183,18 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -175,17 +183,18 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
175 } 183 }
176 ttl = computeTtl(ttl); 184 ttl = computeTtl(ttl);
177 long partition = toPartitionTs(tsKvEntryTs); 185 long partition = toPartitionTs(tsKvEntryTs);
178 - log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key);  
179 - BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt()).bind());  
180 - stmtBuilder.setString(0, entityId.getEntityType().name())  
181 - .setUuid(1, entityId.getId())  
182 - .setLong(2, partition)  
183 - .setString(3, key);  
184 - if (ttl > 0) {  
185 - stmtBuilder.setInt(4, (int) ttl); 186 + if (cassandraTsPartitionsCache == null) {
  187 + return doSavePartition(tenantId, entityId, key, ttl, partition);
  188 + } else {
  189 + CassandraPartitionCacheKey partitionSearchKey = new CassandraPartitionCacheKey(entityId, key, partition);
  190 + if (!cassandraTsPartitionsCache.has(partitionSearchKey)) {
  191 + ListenableFuture<Integer> result = doSavePartition(tenantId, entityId, key, ttl, partition);
  192 + Futures.addCallback(result, new CacheCallback<>(partitionSearchKey), MoreExecutors.directExecutor());
  193 + return result;
  194 + } else {
  195 + return Futures.immediateFuture(0);
  196 + }
186 } 197 }
187 - BoundStatement stmt = stmtBuilder.build();  
188 - return getFuture(executeAsyncWrite(tenantId, stmt), rs -> 0);  
189 } 198 }
190 199
191 @Override 200 @Override
@@ -461,6 +470,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -461,6 +470,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
461 return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); 470 return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null);
462 } 471 }
463 472
  473 + private ListenableFuture<Integer> doSavePartition(TenantId tenantId, EntityId entityId, String key, long ttl, long partition) {
  474 + log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key);
  475 + PreparedStatement preparedStatement = ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt();
  476 + BoundStatement stmt = preparedStatement.bind();
  477 + stmt = stmt.setString(0, entityId.getEntityType().name())
  478 + .setUuid(1, entityId.getId())
  479 + .setLong(2, partition)
  480 + .setString(3, key);
  481 + if (ttl > 0) {
  482 + stmt = stmt.setInt(4, (int) ttl);
  483 + }
  484 + return getFuture(executeAsyncWrite(tenantId, stmt), rs -> 0);
  485 + }
  486 +
  487 + private class CacheCallback<Void> implements FutureCallback<Void> {
  488 + private final CassandraPartitionCacheKey key;
  489 +
  490 + private CacheCallback(CassandraPartitionCacheKey key) {
  491 + this.key = key;
  492 + }
  493 +
  494 + @Override
  495 + public void onSuccess(Void result) {
  496 + cassandraTsPartitionsCache.put(key);
  497 + }
  498 +
  499 + @Override
  500 + public void onFailure(Throwable t) {
  501 +
  502 + }
  503 + }
  504 +
464 private long computeTtl(long ttl) { 505 private long computeTtl(long ttl) {
465 if (systemTtl > 0) { 506 if (systemTtl > 0) {
466 if (ttl == 0) { 507 if (ttl == 0) {
@@ -87,6 +87,11 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes @@ -87,6 +87,11 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
87 } 87 }
88 88
89 @Override 89 @Override
  90 + public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
  91 + return Collections.emptyList();
  92 + }
  93 +
  94 + @Override
90 public ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { 95 public ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
91 BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind()); 96 BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind());
92 stmtBuilder.setString(0, entityId.getEntityType().name()) 97 stmtBuilder.setString(0, entityId.getEntityType().name())
  1 +/**
  2 + * Copyright © 2016-2020 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 +package org.thingsboard.server.dao.timeseries;
  17 +
  18 +import lombok.AllArgsConstructor;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.data.id.EntityId;
  21 +
  22 +@Data
  23 +@AllArgsConstructor
  24 +public class CassandraPartitionCacheKey {
  25 +
  26 + private EntityId entityId;
  27 + private String key;
  28 + private long partition;
  29 +
  30 +}
  1 +/**
  2 + * Copyright © 2016-2020 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 +package org.thingsboard.server.dao.timeseries;
  17 +
  18 +import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
  19 +import com.github.benmanes.caffeine.cache.Caffeine;
  20 +
  21 +import java.util.concurrent.CompletableFuture;
  22 +
  23 +public class CassandraTsPartitionsCache {
  24 +
  25 + private AsyncLoadingCache<CassandraPartitionCacheKey, Boolean> partitionsCache;
  26 +
  27 + public CassandraTsPartitionsCache(long maxCacheSize) {
  28 + this.partitionsCache = Caffeine.newBuilder()
  29 + .maximumSize(maxCacheSize)
  30 + .buildAsync(key -> {
  31 + throw new IllegalStateException("'get' methods calls are not supported!");
  32 + });
  33 + }
  34 +
  35 + public boolean has(CassandraPartitionCacheKey key) {
  36 + return partitionsCache.getIfPresent(key) != null;
  37 + }
  38 +
  39 + public void put(CassandraPartitionCacheKey key) {
  40 + partitionsCache.put(key, CompletableFuture.completedFuture(true));
  41 + }
  42 +}
@@ -35,4 +35,6 @@ public interface TimeseriesLatestDao { @@ -35,4 +35,6 @@ public interface TimeseriesLatestDao {
35 ListenableFuture<Void> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); 35 ListenableFuture<Void> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
36 36
37 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); 37 List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
  38 +
  39 + List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
38 } 40 }
  1 +/**
  2 + * Copyright © 2016-2020 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 +package org.thingsboard.server.dao.nosql;
  17 +
  18 +import com.datastax.oss.driver.api.core.ConsistencyLevel;
  19 +import com.datastax.oss.driver.api.core.cql.BoundStatement;
  20 +import com.datastax.oss.driver.api.core.cql.PreparedStatement;
  21 +import com.datastax.oss.driver.api.core.cql.Statement;
  22 +import com.google.common.util.concurrent.Futures;
  23 +import org.junit.Before;
  24 +import org.junit.Test;
  25 +import org.junit.runner.RunWith;
  26 +import org.mockito.Mock;
  27 +import org.mockito.Spy;
  28 +import org.mockito.runners.MockitoJUnitRunner;
  29 +import org.springframework.core.env.Environment;
  30 +import org.springframework.test.util.ReflectionTestUtils;
  31 +import org.thingsboard.server.common.data.id.TenantId;
  32 +import org.thingsboard.server.dao.cassandra.CassandraCluster;
  33 +import org.thingsboard.server.dao.cassandra.guava.GuavaSession;
  34 +import org.thingsboard.server.dao.timeseries.CassandraBaseTimeseriesDao;
  35 +
  36 +import java.util.UUID;
  37 +
  38 +import static org.mockito.Matchers.any;
  39 +import static org.mockito.Matchers.anyInt;
  40 +import static org.mockito.Matchers.anyString;
  41 +import static org.mockito.Mockito.doReturn;
  42 +import static org.mockito.Mockito.times;
  43 +import static org.mockito.Mockito.verify;
  44 +import static org.mockito.Mockito.when;
  45 +
  46 +@RunWith(MockitoJUnitRunner.class)
  47 +public class CassandraPartitionsCacheTest {
  48 +
  49 + @Spy
  50 + private CassandraBaseTimeseriesDao cassandraBaseTimeseriesDao;
  51 +
  52 + @Mock
  53 + private PreparedStatement preparedStatement;
  54 +
  55 + @Mock
  56 + private BoundStatement boundStatement;
  57 +
  58 + @Mock
  59 + private Environment environment;
  60 +
  61 + @Mock
  62 + private CassandraBufferedRateExecutor rateLimiter;
  63 +
  64 + @Mock
  65 + private CassandraCluster cluster;
  66 +
  67 + @Mock
  68 + private GuavaSession session;
  69 +
  70 + @Before
  71 + public void setUp() throws Exception {
  72 + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "partitioning", "MONTHS");
  73 + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "partitionsCacheSize", 100000);
  74 + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "systemTtl", 0);
  75 + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "setNullValuesEnabled", false);
  76 + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "environment", environment);
  77 + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "rateLimiter", rateLimiter);
  78 + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "cluster", cluster);
  79 +
  80 + when(cluster.getDefaultReadConsistencyLevel()).thenReturn(ConsistencyLevel.ONE);
  81 + when(cluster.getDefaultWriteConsistencyLevel()).thenReturn(ConsistencyLevel.ONE);
  82 + when(cluster.getSession()).thenReturn(session);
  83 + when(session.prepare(anyString())).thenReturn(preparedStatement);
  84 +
  85 + when(preparedStatement.bind()).thenReturn(boundStatement);
  86 +
  87 + when(boundStatement.setString(anyInt(), anyString())).thenReturn(boundStatement);
  88 + when(boundStatement.setUuid(anyInt(), any(UUID.class))).thenReturn(boundStatement);
  89 + when(boundStatement.setLong(anyInt(), any(Long.class))).thenReturn(boundStatement);
  90 +
  91 + doReturn(Futures.immediateFuture(0)).when(cassandraBaseTimeseriesDao).getFuture(any(TbResultSetFuture.class), any());
  92 + }
  93 +
  94 + @Test
  95 + public void testPartitionSave() throws Exception {
  96 + cassandraBaseTimeseriesDao.init();
  97 +
  98 + UUID id = UUID.randomUUID();
  99 + TenantId tenantId = new TenantId(id);
  100 + long tsKvEntryTs = System.currentTimeMillis();
  101 +
  102 + for (int i = 0; i < 50000; i++) {
  103 + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0);
  104 + }
  105 + for (int i = 0; i < 60000; i++) {
  106 + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0);
  107 + }
  108 + verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class));
  109 + }
  110 +}
@@ -54,6 +54,8 @@ cassandra.query.default_fetch_size=2000 @@ -54,6 +54,8 @@ cassandra.query.default_fetch_size=2000
54 54
55 cassandra.query.ts_key_value_partitioning=HOURS 55 cassandra.query.ts_key_value_partitioning=HOURS
56 56
  57 +cassandra.query.ts_key_value_partitions_max_cache_size=100000
  58 +
57 cassandra.query.ts_key_value_ttl=0 59 cassandra.query.ts_key_value_ttl=0
58 60
59 cassandra.query.debug_events_ttl=604800 61 cassandra.query.debug_events_ttl=604800
@@ -41,10 +41,16 @@ import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry. @@ -41,10 +41,16 @@ import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.
41 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; 41 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
42 import { RuleChainService } from '@core/http/rule-chain.service'; 42 import { RuleChainService } from '@core/http/rule-chain.service';
43 import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models'; 43 import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models';
44 -import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; 44 +import { DataKey, Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models';
45 import { UtilsService } from '@core/services/utils.service'; 45 import { UtilsService } from '@core/services/utils.service';
46 import { AliasFilterType, EntityAlias, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models'; 46 import { AliasFilterType, EntityAlias, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models';
47 -import { entityFields, EntityInfo, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; 47 +import {
  48 + EntitiesKeysByQuery,
  49 + entityFields,
  50 + EntityInfo,
  51 + ImportEntitiesResultInfo,
  52 + ImportEntityData
  53 +} from '@shared/models/entity.models';
48 import { EntityRelationService } from '@core/http/entity-relation.service'; 54 import { EntityRelationService } from '@core/http/entity-relation.service';
49 import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; 55 import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
50 import { Asset } from '@shared/models/asset.models'; 56 import { Asset } from '@shared/models/asset.models';
@@ -376,6 +382,13 @@ export class EntityService { @@ -376,6 +382,13 @@ export class EntityService {
376 return this.http.post<PageData<EntityData>>('/api/entitiesQuery/find', query, defaultHttpOptionsFromConfig(config)); 382 return this.http.post<PageData<EntityData>>('/api/entitiesQuery/find', query, defaultHttpOptionsFromConfig(config));
377 } 383 }
378 384
  385 + public findEntityKeysByQuery(query: EntityDataQuery, attributes = true, timeseries = true,
  386 + config?: RequestConfig): Observable<EntitiesKeysByQuery> {
  387 + return this.http.post<EntitiesKeysByQuery>(
  388 + `/api/entitiesQuery/find/keys?attributes=${attributes}&timeseries=${timeseries}`,
  389 + query, defaultHttpOptionsFromConfig(config));
  390 + }
  391 +
379 public findAlarmDataByQuery(query: AlarmDataQuery, config?: RequestConfig): Observable<PageData<AlarmData>> { 392 public findAlarmDataByQuery(query: AlarmDataQuery, config?: RequestConfig): Observable<PageData<AlarmData>> {
380 return this.http.post<PageData<AlarmData>>('/api/alarmsQuery/find', query, defaultHttpOptionsFromConfig(config)); 393 return this.http.post<PageData<AlarmData>>('/api/alarmsQuery/find', query, defaultHttpOptionsFromConfig(config));
381 } 394 }
@@ -595,7 +608,7 @@ export class EntityService { @@ -595,7 +608,7 @@ export class EntityService {
595 return entityTypes; 608 return entityTypes;
596 } 609 }
597 610
598 - private getEntityFieldKeys(entityType: EntityType, searchText: string): Array<string> { 611 + private getEntityFieldKeys(entityType: EntityType, searchText: string = ''): Array<string> {
599 const entityFieldKeys: string[] = [entityFields.createdTime.keyName]; 612 const entityFieldKeys: string[] = [entityFields.createdTime.keyName];
600 const query = searchText.toLowerCase(); 613 const query = searchText.toLowerCase();
601 switch (entityType) { 614 switch (entityType) {
@@ -637,7 +650,7 @@ export class EntityService { @@ -637,7 +650,7 @@ export class EntityService {
637 return query ? entityFieldKeys.filter((entityField) => entityField.toLowerCase().indexOf(query) === 0) : entityFieldKeys; 650 return query ? entityFieldKeys.filter((entityField) => entityField.toLowerCase().indexOf(query) === 0) : entityFieldKeys;
638 } 651 }
639 652
640 - private getAlarmKeys(searchText: string): Array<string> { 653 + private getAlarmKeys(searchText: string = ''): Array<string> {
641 const alarmKeys: string[] = Object.keys(alarmFields); 654 const alarmKeys: string[] = Object.keys(alarmFields);
642 const query = searchText.toLowerCase(); 655 const query = searchText.toLowerCase();
643 return query ? alarmKeys.filter((alarmField) => alarmField.toLowerCase().indexOf(query) === 0) : alarmKeys; 656 return query ? alarmKeys.filter((alarmField) => alarmField.toLowerCase().indexOf(query) === 0) : alarmKeys;
@@ -672,6 +685,59 @@ export class EntityService { @@ -672,6 +685,59 @@ export class EntityService {
672 ); 685 );
673 } 686 }
674 687
  688 + public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[], config?: RequestConfig): Observable<Array<DataKey>> {
  689 + if (!types.length) {
  690 + return of([]);
  691 + }
  692 + let entitiesKeysByQuery$: Observable<EntitiesKeysByQuery>;
  693 + if (filter !== null && types.some(type => [DataKeyType.timeseries, DataKeyType.attribute].includes(type))) {
  694 + const dataQuery = {
  695 + entityFilter: filter,
  696 + pageLink: createDefaultEntityDataPageLink(100),
  697 + };
  698 + entitiesKeysByQuery$ = this.findEntityKeysByQuery(dataQuery, types.includes(DataKeyType.attribute),
  699 + types.includes(DataKeyType.timeseries), config);
  700 + } else {
  701 + entitiesKeysByQuery$ = of({
  702 + attribute: [],
  703 + timeseries: [],
  704 + entityTypes: [],
  705 + });
  706 + }
  707 + return entitiesKeysByQuery$.pipe(
  708 + map((entitiesKeys) => {
  709 + const dataKeys: Array<DataKey> = [];
  710 + types.forEach(type => {
  711 + let keys: Array<string>;
  712 + switch (type) {
  713 + case DataKeyType.entityField:
  714 + if (entitiesKeys.entityTypes.length) {
  715 + const entitiesFields = [];
  716 + entitiesKeys.entityTypes.forEach(entityType => entitiesFields.push(...this.getEntityFieldKeys(entityType)));
  717 + keys = Array.from(new Set(entitiesFields));
  718 + }
  719 + break;
  720 + case DataKeyType.alarm:
  721 + keys = this.getAlarmKeys();
  722 + break;
  723 + case DataKeyType.attribute:
  724 + case DataKeyType.timeseries:
  725 + if (entitiesKeys[type].length) {
  726 + keys = entitiesKeys[type];
  727 + }
  728 + break;
  729 + }
  730 + if (keys) {
  731 + dataKeys.push(...keys.map(key => {
  732 + return {name: key, type};
  733 + }));
  734 + }
  735 + });
  736 + return dataKeys;
  737 + })
  738 + );
  739 + }
  740 +
675 public createDatasourcesFromSubscriptionsInfo(subscriptionsInfo: Array<SubscriptionInfo>): Array<Datasource> { 741 public createDatasourcesFromSubscriptionsInfo(subscriptionsInfo: Array<SubscriptionInfo>): Array<Datasource> {
676 const datasources = subscriptionsInfo.map(subscriptionInfo => this.createDatasourceFromSubscriptionInfo(subscriptionInfo)); 742 const datasources = subscriptionsInfo.map(subscriptionInfo => this.createDatasourceFromSubscriptionInfo(subscriptionInfo));
677 this.utils.generateColors(datasources); 743 this.utils.generateColors(datasources);
@@ -33,87 +33,91 @@ @@ -33,87 +33,91 @@
33 </button> 33 </button>
34 </div> 34 </div>
35 </mat-expansion-panel-header> 35 </mat-expansion-panel-header>
36 - <div fxLayout="column" fxLayoutGap="0.5em">  
37 - <mat-divider></mat-divider>  
38 - <mat-form-field fxFlex floatLabel="always">  
39 - <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>  
40 - <input required matInput formControlName="alarmType" placeholder="Enter alarm type">  
41 - <mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')">  
42 - {{ 'device-profile.alarm-type-required' | translate }}  
43 - </mat-error>  
44 - <mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('unique')">  
45 - {{ 'device-profile.alarm-type-unique' | translate }}  
46 - </mat-error>  
47 - </mat-form-field>  
48 - </div>  
49 - <mat-expansion-panel class="advanced-settings" [expanded]="false">  
50 - <mat-expansion-panel-header>  
51 - <mat-panel-title>  
52 - <div fxFlex fxLayout="row" fxLayoutAlign="end center">  
53 - <div class="tb-small" translate>device-profile.advanced-settings</div>  
54 - </div>  
55 - </mat-panel-title>  
56 - </mat-expansion-panel-header>  
57 - <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">  
58 - {{ 'device-profile.propagate-alarm' | translate }}  
59 - </mat-checkbox>  
60 - <section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">  
61 - <mat-form-field floatLabel="always" class="mat-block">  
62 - <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>  
63 - <mat-chip-list #relationTypesChipList [disabled]="disabled">  
64 - <mat-chip  
65 - *ngFor="let key of alarmFormGroup.get('propagateRelationTypes').value;"  
66 - (removed)="removeRelationType(key)">  
67 - {{key}}  
68 - <mat-icon matChipRemove>close</mat-icon>  
69 - </mat-chip>  
70 - <input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}"  
71 - style="max-width: 200px;"  
72 - [matChipInputFor]="relationTypesChipList"  
73 - [matChipInputSeparatorKeyCodes]="separatorKeysCodes"  
74 - (matChipInputTokenEnd)="addRelationType($event)"  
75 - [matChipInputAddOnBlur]="true">  
76 - </mat-chip-list>  
77 - <mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint> 36 + <ng-template matExpansionPanelContent>
  37 + <div fxLayout="column" fxLayoutGap="0.5em">
  38 + <mat-divider></mat-divider>
  39 + <mat-form-field fxFlex floatLabel="always">
  40 + <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
  41 + <input required matInput formControlName="alarmType" placeholder="Enter alarm type">
  42 + <mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')">
  43 + {{ 'device-profile.alarm-type-required' | translate }}
  44 + </mat-error>
  45 + <mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('unique')">
  46 + {{ 'device-profile.alarm-type-unique' | translate }}
  47 + </mat-error>
78 </mat-form-field> 48 </mat-form-field>
79 - </section>  
80 - </mat-expansion-panel>  
81 - <div fxFlex fxLayout="column">  
82 - <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.create-alarm-rules</div>  
83 - <tb-create-alarm-rules formControlName="createRules"  
84 - style="padding-bottom: 16px;"  
85 - [deviceProfileId]="deviceProfileId">  
86 - </tb-create-alarm-rules>  
87 - <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div>  
88 - <div fxLayout="row" fxLayoutGap="8px;" fxLayoutAlign="start center"  
89 - [fxShow]="alarmFormGroup.get('clearRule').value"  
90 - style="padding-bottom: 8px;">  
91 - <div class="clear-alarm-rule" fxFlex fxLayout="row">  
92 - <tb-alarm-rule formControlName="clearRule" fxFlex [deviceProfileId]="deviceProfileId">  
93 - </tb-alarm-rule>  
94 - </div>  
95 - <button *ngIf="!disabled"  
96 - mat-icon-button color="primary" style="min-width: 40px;"  
97 - type="button"  
98 - (click)="removeClearAlarmRule()"  
99 - matTooltip="{{ 'action.remove' | translate }}"  
100 - matTooltipPosition="above">  
101 - <mat-icon>remove_circle_outline</mat-icon>  
102 - </button>  
103 </div> 49 </div>
104 - <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">  
105 - <span translate fxLayoutAlign="center center" style="margin: 16px 0"  
106 - class="tb-prompt">device-profile.no-clear-alarm-rule</span>  
107 - </div>  
108 - <div *ngIf="!disabled" [fxShow]="!alarmFormGroup.get('clearRule').value">  
109 - <button mat-stroked-button color="primary"  
110 - type="button"  
111 - (click)="addClearAlarmRule()"  
112 - matTooltip="{{ 'device-profile.add-clear-alarm-rule' | translate }}"  
113 - matTooltipPosition="above">  
114 - <mat-icon class="button-icon">add_circle_outline</mat-icon>  
115 - {{ 'device-profile.add-clear-alarm-rule' | translate }}  
116 - </button> 50 + <mat-expansion-panel class="advanced-settings" [expanded]="false">
  51 + <mat-expansion-panel-header>
  52 + <mat-panel-title>
  53 + <div fxFlex fxLayout="row" fxLayoutAlign="end center">
  54 + <div class="tb-small" translate>device-profile.advanced-settings</div>
  55 + </div>
  56 + </mat-panel-title>
  57 + </mat-expansion-panel-header>
  58 + <ng-template matExpansionPanelContent>
  59 + <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
  60 + {{ 'device-profile.propagate-alarm' | translate }}
  61 + </mat-checkbox>
  62 + <section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">
  63 + <mat-form-field floatLabel="always" class="mat-block">
  64 + <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
  65 + <mat-chip-list #relationTypesChipList [disabled]="disabled">
  66 + <mat-chip
  67 + *ngFor="let key of alarmFormGroup.get('propagateRelationTypes').value;"
  68 + (removed)="removeRelationType(key)">
  69 + {{key}}
  70 + <mat-icon matChipRemove>close</mat-icon>
  71 + </mat-chip>
  72 + <input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}"
  73 + style="max-width: 200px;"
  74 + [matChipInputFor]="relationTypesChipList"
  75 + [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
  76 + (matChipInputTokenEnd)="addRelationType($event)"
  77 + [matChipInputAddOnBlur]="true">
  78 + </mat-chip-list>
  79 + <mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
  80 + </mat-form-field>
  81 + </section>
  82 + </ng-template>
  83 + </mat-expansion-panel>
  84 + <div fxFlex fxLayout="column">
  85 + <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.create-alarm-rules</div>
  86 + <tb-create-alarm-rules formControlName="createRules"
  87 + style="padding-bottom: 16px;"
  88 + [deviceProfileId]="deviceProfileId">
  89 + </tb-create-alarm-rules>
  90 + <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div>
  91 + <div fxLayout="row" fxLayoutGap="8px;" fxLayoutAlign="start center"
  92 + [fxShow]="alarmFormGroup.get('clearRule').value"
  93 + style="padding-bottom: 8px;">
  94 + <div class="clear-alarm-rule" fxFlex fxLayout="row">
  95 + <tb-alarm-rule formControlName="clearRule" fxFlex [deviceProfileId]="deviceProfileId">
  96 + </tb-alarm-rule>
  97 + </div>
  98 + <button *ngIf="!disabled"
  99 + mat-icon-button color="primary" style="min-width: 40px;"
  100 + type="button"
  101 + (click)="removeClearAlarmRule()"
  102 + matTooltip="{{ 'action.remove' | translate }}"
  103 + matTooltipPosition="above">
  104 + <mat-icon>remove_circle_outline</mat-icon>
  105 + </button>
  106 + </div>
  107 + <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">
  108 + <span translate fxLayoutAlign="center center" style="margin: 16px 0"
  109 + class="tb-prompt">device-profile.no-clear-alarm-rule</span>
  110 + </div>
  111 + <div *ngIf="!disabled" [fxShow]="!alarmFormGroup.get('clearRule').value">
  112 + <button mat-stroked-button color="primary"
  113 + type="button"
  114 + (click)="addClearAlarmRule()"
  115 + matTooltip="{{ 'device-profile.add-clear-alarm-rule' | translate }}"
  116 + matTooltipPosition="above">
  117 + <mat-icon class="button-icon">add_circle_outline</mat-icon>
  118 + {{ 'device-profile.add-clear-alarm-rule' | translate }}
  119 + </button>
  120 + </div>
117 </div> 121 </div>
118 - </div> 122 + </ng-template>
119 </mat-expansion-panel> 123 </mat-expansion-panel>
@@ -91,10 +91,12 @@ @@ -91,10 +91,12 @@
91 <div translate>device-profile.profile-configuration</div> 91 <div translate>device-profile.profile-configuration</div>
92 </mat-panel-title> 92 </mat-panel-title>
93 </mat-expansion-panel-header> 93 </mat-expansion-panel-header>
94 - <tb-device-profile-configuration  
95 - formControlName="configuration"  
96 - required>  
97 - </tb-device-profile-configuration> 94 + <ng-template matExpansionPanelContent>
  95 + <tb-device-profile-configuration
  96 + formControlName="configuration"
  97 + required>
  98 + </tb-device-profile-configuration>
  99 + </ng-template>
98 </mat-expansion-panel> 100 </mat-expansion-panel>
99 <mat-expansion-panel *ngIf="displayTransportConfiguration" [expanded]="true"> 101 <mat-expansion-panel *ngIf="displayTransportConfiguration" [expanded]="true">
100 <mat-expansion-panel-header> 102 <mat-expansion-panel-header>
@@ -102,10 +104,12 @@ @@ -102,10 +104,12 @@
102 <div translate>device-profile.transport-configuration</div> 104 <div translate>device-profile.transport-configuration</div>
103 </mat-panel-title> 105 </mat-panel-title>
104 </mat-expansion-panel-header> 106 </mat-expansion-panel-header>
105 - <tb-device-profile-transport-configuration  
106 - formControlName="transportConfiguration"  
107 - required>  
108 - </tb-device-profile-transport-configuration> 107 + <ng-template matExpansionPanelContent>
  108 + <tb-device-profile-transport-configuration
  109 + formControlName="transportConfiguration"
  110 + required>
  111 + </tb-device-profile-transport-configuration>
  112 + </ng-template>
109 </mat-expansion-panel> 113 </mat-expansion-panel>
110 <mat-expansion-panel [expanded]="false"> 114 <mat-expansion-panel [expanded]="false">
111 <mat-expansion-panel-header> 115 <mat-expansion-panel-header>
@@ -115,10 +119,12 @@ @@ -115,10 +119,12 @@
115 entityForm.get('profileData.alarms').value.length : 0} }}</div> 119 entityForm.get('profileData.alarms').value.length : 0} }}</div>
116 </mat-panel-title> 120 </mat-panel-title>
117 </mat-expansion-panel-header> 121 </mat-expansion-panel-header>
118 - <tb-device-profile-alarms  
119 - formControlName="alarms"  
120 - [deviceProfileId]="deviceProfileId">  
121 - </tb-device-profile-alarms> 122 + <ng-template matExpansionPanelContent>
  123 + <tb-device-profile-alarms
  124 + formControlName="alarms"
  125 + [deviceProfileId]="deviceProfileId">
  126 + </tb-device-profile-alarms>
  127 + </ng-template>
122 </mat-expansion-panel> 128 </mat-expansion-panel>
123 <mat-expansion-panel [expanded]="true"> 129 <mat-expansion-panel [expanded]="true">
124 <mat-expansion-panel-header> 130 <mat-expansion-panel-header>
@@ -126,9 +132,11 @@ @@ -126,9 +132,11 @@
126 <div translate>device-profile.device-provisioning</div> 132 <div translate>device-profile.device-provisioning</div>
127 </mat-panel-title> 133 </mat-panel-title>
128 </mat-expansion-panel-header> 134 </mat-expansion-panel-header>
129 - <tb-device-profile-provision-configuration  
130 - formControlName="provisionConfiguration">  
131 - </tb-device-profile-provision-configuration> 135 + <ng-template matExpansionPanelContent>
  136 + <tb-device-profile-provision-configuration
  137 + formControlName="provisionConfiguration">
  138 + </tb-device-profile-provision-configuration>
  139 + </ng-template>
132 </mat-expansion-panel> 140 </mat-expansion-panel>
133 </mat-accordion> 141 </mat-accordion>
134 </div> 142 </div>
@@ -22,9 +22,11 @@ @@ -22,9 +22,11 @@
22 <div translate>tenant-profile.profile-configuration</div> 22 <div translate>tenant-profile.profile-configuration</div>
23 </mat-panel-title> 23 </mat-panel-title>
24 </mat-expansion-panel-header> 24 </mat-expansion-panel-header>
25 - <tb-tenant-profile-configuration  
26 - formControlName="configuration"  
27 - required>  
28 - </tb-tenant-profile-configuration> 25 + <ng-template matExpansionPanelContent>
  26 + <tb-tenant-profile-configuration
  27 + formControlName="configuration"
  28 + required>
  29 + </tb-tenant-profile-configuration>
  30 + </ng-template>
29 </mat-expansion-panel> 31 </mat-expansion-panel>
30 </form> 32 </form>
@@ -36,7 +36,7 @@ import { EntityService } from '@core/http/entity.service'; @@ -36,7 +36,7 @@ import { EntityService } from '@core/http/entity.service';
36 import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; 36 import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models';
37 import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 37 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
38 import { Observable, of } from 'rxjs'; 38 import { Observable, of } from 'rxjs';
39 -import { map, mergeMap, tap } from 'rxjs/operators'; 39 +import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';
40 import { alarmFields } from '@shared/models/alarm.models'; 40 import { alarmFields } from '@shared/models/alarm.models';
41 import { JsFuncComponent } from '@shared/components/js-func.component'; 41 import { JsFuncComponent } from '@shared/components/js-func.component';
42 import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; 42 import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
@@ -95,6 +95,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @@ -95,6 +95,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
95 95
96 filteredKeys: Observable<Array<string>>; 96 filteredKeys: Observable<Array<string>>;
97 private latestKeySearchResult: Array<string> = null; 97 private latestKeySearchResult: Array<string> = null;
  98 + private fetchObservable$: Observable<Array<string>> = null;
98 99
99 keySearchText = ''; 100 keySearchText = '';
100 101
@@ -205,31 +206,42 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @@ -205,31 +206,42 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
205 } 206 }
206 207
207 private fetchKeys(searchText?: string): Observable<Array<string>> { 208 private fetchKeys(searchText?: string): Observable<Array<string>> {
208 - if (this.latestKeySearchResult === null || this.keySearchText !== searchText) { 209 + if (this.keySearchText !== searchText || this.latestKeySearchResult === null) {
209 this.keySearchText = searchText; 210 this.keySearchText = searchText;
210 - let fetchObservable: Observable<Array<DataKey>> = null; 211 + const dataKeyFilter = this.createKeyFilter(this.keySearchText);
  212 + return this.getKeys().pipe(
  213 + map(name => name.filter(dataKeyFilter)),
  214 + tap(res => this.latestKeySearchResult = res)
  215 + );
  216 + }
  217 + return of(this.latestKeySearchResult);
  218 + }
  219 +
  220 + private getKeys() {
  221 + if (this.fetchObservable$ === null) {
  222 + let fetchObservable: Observable<Array<DataKey>>;
211 if (this.modelValue.type === DataKeyType.alarm) { 223 if (this.modelValue.type === DataKeyType.alarm) {
212 - const dataKeyFilter = this.createDataKeyFilter(this.keySearchText);  
213 - fetchObservable = of(this.alarmKeys.filter(dataKeyFilter)); 224 + fetchObservable = of(this.alarmKeys);
214 } else { 225 } else {
215 if (this.entityAliasId) { 226 if (this.entityAliasId) {
216 const dataKeyTypes = [this.modelValue.type]; 227 const dataKeyTypes = [this.modelValue.type];
217 - fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.keySearchText, dataKeyTypes); 228 + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
218 } else { 229 } else {
219 fetchObservable = of([]); 230 fetchObservable = of([]);
220 } 231 }
221 } 232 }
222 - return fetchObservable.pipe( 233 + this.fetchObservable$ = fetchObservable.pipe(
223 map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), 234 map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)),
224 - tap(res => this.latestKeySearchResult = res) 235 + publishReplay(1),
  236 + refCount()
225 ); 237 );
226 } 238 }
227 - return of(this.latestKeySearchResult); 239 + return this.fetchObservable$;
228 } 240 }
229 241
230 - private createDataKeyFilter(query: string): (key: DataKey) => boolean { 242 + private createKeyFilter(query: string): (key: string) => boolean {
231 const lowercaseQuery = query.toLowerCase(); 243 const lowercaseQuery = query.toLowerCase();
232 - return key => key.name.toLowerCase().indexOf(lowercaseQuery) === 0; 244 + return key => key.toLowerCase().startsWith(lowercaseQuery);
233 } 245 }
234 246
235 public validateOnSubmit() { 247 public validateOnSubmit() {
@@ -20,5 +20,5 @@ import { Observable } from 'rxjs'; @@ -20,5 +20,5 @@ import { Observable } from 'rxjs';
20 20
21 export interface DataKeysCallbacks { 21 export interface DataKeysCallbacks {
22 generateDataKey: (chip: any, type: DataKeyType) => DataKey; 22 generateDataKey: (chip: any, type: DataKeyType) => DataKey;
23 - fetchEntityKeys: (entityAliasId: string, query: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>; 23 + fetchEntityKeys: (entityAliasId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
24 } 24 }
@@ -38,7 +38,7 @@ import { @@ -38,7 +38,7 @@ import {
38 Validators 38 Validators
39 } from '@angular/forms'; 39 } from '@angular/forms';
40 import { Observable, of } from 'rxjs'; 40 import { Observable, of } from 'rxjs';
41 -import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; 41 +import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
42 import { Store } from '@ngrx/store'; 42 import { Store } from '@ngrx/store';
43 import { AppState } from '@app/core/core.state'; 43 import { AppState } from '@app/core/core.state';
44 import { TranslateService } from '@ngx-translate/core'; 44 import { TranslateService } from '@ngx-translate/core';
@@ -142,6 +142,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @@ -142,6 +142,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
142 142
143 searchText = ''; 143 searchText = '';
144 private latestSearchTextResult: Array<DataKey> = null; 144 private latestSearchTextResult: Array<DataKey> = null;
  145 + private fetchObservable$: Observable<Array<DataKey>> = null;
145 146
146 private dirty = false; 147 private dirty = false;
147 148
@@ -260,6 +261,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @@ -260,6 +261,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
260 if (!change.firstChange && change.currentValue !== change.previousValue) { 261 if (!change.firstChange && change.currentValue !== change.previousValue) {
261 if (propName === 'entityAliasId') { 262 if (propName === 'entityAliasId') {
262 this.searchText = ''; 263 this.searchText = '';
  264 + this.fetchObservable$ = null;
263 this.latestSearchTextResult = null; 265 this.latestSearchTextResult = null;
264 this.dirty = true; 266 this.dirty = true;
265 } else if (['widgetType', 'datasourceType'].includes(propName)) { 267 } else if (['widgetType', 'datasourceType'].includes(propName)) {
@@ -405,14 +407,24 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @@ -405,14 +407,24 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
405 return key ? key.name : undefined; 407 return key ? key.name : undefined;
406 } 408 }
407 409
408 - fetchKeys(searchText?: string): Observable<Array<DataKey>> {  
409 - if (this.latestSearchTextResult === null || this.searchText !== searchText) { 410 + private fetchKeys(searchText?: string): Observable<Array<DataKey>> {
  411 + if (this.searchText !== searchText || this.latestSearchTextResult === null) {
410 this.searchText = searchText; 412 this.searchText = searchText;
411 - let fetchObservable: Observable<Array<DataKey>> = null; 413 + const dataKeyFilter = this.createDataKeyFilter(this.searchText);
  414 + return this.getKeys().pipe(
  415 + map(name => name.filter(dataKeyFilter)),
  416 + tap(res => this.latestSearchTextResult = res)
  417 + );
  418 + }
  419 + return of(this.latestSearchTextResult);
  420 + }
  421 +
  422 + private getKeys(): Observable<Array<DataKey>> {
  423 + if (this.fetchObservable$ === null) {
  424 + let fetchObservable: Observable<Array<DataKey>>;
412 if (this.datasourceType === DatasourceType.function) { 425 if (this.datasourceType === DatasourceType.function) {
413 - const dataKeyFilter = this.createDataKeyFilter(this.searchText);  
414 const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; 426 const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
415 - fetchObservable = of(targetKeysList.filter(dataKeyFilter)); 427 + fetchObservable = of(targetKeysList);
416 } else { 428 } else {
417 if (this.entityAliasId) { 429 if (this.entityAliasId) {
418 const dataKeyTypes = [DataKeyType.timeseries]; 430 const dataKeyTypes = [DataKeyType.timeseries];
@@ -420,24 +432,25 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @@ -420,24 +432,25 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
420 dataKeyTypes.push(DataKeyType.attribute); 432 dataKeyTypes.push(DataKeyType.attribute);
421 dataKeyTypes.push(DataKeyType.entityField); 433 dataKeyTypes.push(DataKeyType.entityField);
422 if (this.widgetType === widgetType.alarm) { 434 if (this.widgetType === widgetType.alarm) {
423 - dataKeyTypes.push(DataKeyType.alarm); 435 + dataKeyTypes.push(DataKeyType.alarm);
424 } 436 }
425 } 437 }
426 - fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.searchText, dataKeyTypes); 438 + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
427 } else { 439 } else {
428 fetchObservable = of([]); 440 fetchObservable = of([]);
429 } 441 }
430 } 442 }
431 - return fetchObservable.pipe(  
432 - tap(res => this.latestSearchTextResult = res) 443 + this.fetchObservable$ = fetchObservable.pipe(
  444 + publishReplay(1),
  445 + refCount()
433 ); 446 );
434 } 447 }
435 - return of(this.latestSearchTextResult); 448 + return this.fetchObservable$;
436 } 449 }
437 450
438 private createDataKeyFilter(query: string): (key: DataKey) => boolean { 451 private createDataKeyFilter(query: string): (key: DataKey) => boolean {
439 const lowercaseQuery = query.toLowerCase(); 452 const lowercaseQuery = query.toLowerCase();
440 - return key => key.name.toLowerCase().indexOf(lowercaseQuery) === 0; 453 + return key => key.name.toLowerCase().startsWith(lowercaseQuery);
441 } 454 }
442 455
443 textIsNotEmpty(text: string): boolean { 456 textIsNotEmpty(text: string): boolean {
@@ -608,26 +608,32 @@ export default abstract class LeafletMap { @@ -608,26 +608,32 @@ export default abstract class LeafletMap {
608 return polygon; 608 return polygon;
609 } 609 }
610 610
611 - updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) { 611 + updatePoints(pointsData: FormattedData[][], getTooltip: (point: FormattedData) => string) {
  612 + if(pointsData.length) {
612 if (this.points) { 613 if (this.points) {
613 - this.map.removeLayer(this.points); 614 + this.map.removeLayer(this.points);
614 } 615 }
615 this.points = new FeatureGroup(); 616 this.points = new FeatureGroup();
616 - pointsData.filter(pdata => !!this.convertPosition(pdata)).forEach(data => {  
617 - const point = L.circleMarker(this.convertPosition(data), {  
618 - color: this.options.pointColor,  
619 - radius: this.options.pointSize  
620 - });  
621 - if (!this.options.pointTooltipOnRightPanel) {  
622 - point.on('click', () => getTooltip(data));  
623 - }  
624 - else {  
625 - createTooltip(point, this.options, data.$datasource, getTooltip(data, false));  
626 - }  
627 - this.points.addLayer(point); 617 + }
  618 + for(let i = 0; i < pointsData.length; i++) {
  619 + const pointsList = pointsData[i];
  620 + pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => {
  621 + const point = L.circleMarker(this.convertPosition(data), {
  622 + color: this.options.pointColor,
  623 + radius: this.options.pointSize
  624 + });
  625 + if (!this.options.pointTooltipOnRightPanel) {
  626 + point.on('click', () => getTooltip(data));
  627 + } else {
  628 + createTooltip(point, this.options, data.$datasource, getTooltip(data));
  629 + }
  630 + this.points.addLayer(point);
628 }); 631 });
  632 + }
  633 + if(pointsData.length) {
629 this.map.addLayer(this.points); 634 this.map.addLayer(this.points);
630 } 635 }
  636 + }
631 637
632 // Polyline 638 // Polyline
633 639
@@ -28,8 +28,12 @@ @@ -28,8 +28,12 @@
28 </button> 28 </button>
29 </div> 29 </div>
30 <div class="trip-animation-tooltip md-whiteframe-z4" fxLayout="column" 30 <div class="trip-animation-tooltip md-whiteframe-z4" fxLayout="column"
31 - [ngClass]="{'trip-animation-tooltip-hidden':!visibleTooltip}" [innerHTML]="mainTooltip"  
32 - [ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}"> 31 + [ngClass]="{'trip-animation-tooltip-hidden':!visibleTooltip}"
  32 + [ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}">
  33 + <div *ngFor="let mainTooltip of mainTooltips"
  34 + [innerHTML]="mainTooltip"
  35 + style="padding: 10px 0">
  36 + </div>
33 </div> 37 </div>
34 </div> 38 </div>
35 <tb-history-selector *ngIf="historicalData" 39 <tb-history-selector *ngIf="historicalData"
@@ -73,6 +73,9 @@ @@ -73,6 +73,9 @@
73 } 73 }
74 74
75 .trip-animation-tooltip { 75 .trip-animation-tooltip {
  76 + display: flex;
  77 + overflow: auto;
  78 + max-height: 90%;
76 position: absolute; 79 position: absolute;
77 top: 30px; 80 top: 30px;
78 right: 0; 81 right: 0;
@@ -86,4 +89,4 @@ @@ -86,4 +89,4 @@
86 } 89 }
87 } 90 }
88 } 91 }
89 -}  
  92 +}
@@ -47,6 +47,9 @@ import moment from 'moment'; @@ -47,6 +47,9 @@ import moment from 'moment';
47 import { isUndefined } from '@core/utils'; 47 import { isUndefined } from '@core/utils';
48 import { ResizeObserver } from '@juggle/resize-observer'; 48 import { ResizeObserver } from '@juggle/resize-observer';
49 49
  50 +interface dataMap {
  51 + [key: string] : FormattedData
  52 +}
50 53
51 @Component({ 54 @Component({
52 // tslint:disable-next-line:component-selector 55 // tslint:disable-next-line:component-selector
@@ -70,7 +73,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy @@ -70,7 +73,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
70 interpolatedTimeData = []; 73 interpolatedTimeData = [];
71 widgetConfig: WidgetConfig; 74 widgetConfig: WidgetConfig;
72 settings: TripAnimationSettings; 75 settings: TripAnimationSettings;
73 - mainTooltip = ''; 76 + mainTooltips = [];
74 visibleTooltip = false; 77 visibleTooltip = false;
75 activeTrip: FormattedData; 78 activeTrip: FormattedData;
76 label: string; 79 label: string;
@@ -115,7 +118,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy @@ -115,7 +118,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
115 this.historicalData = parseArray(this.ctx.data).filter(arr => arr.length); 118 this.historicalData = parseArray(this.ctx.data).filter(arr => arr.length);
116 if (this.historicalData.length) { 119 if (this.historicalData.length) {
117 this.calculateIntervals(); 120 this.calculateIntervals();
118 - this.timeUpdated(this.currentTime && this.currentTime > this.minTime ? this.currentTime : this.minTime); 121 + this.timeUpdated(this.minTime);
119 } 122 }
120 this.mapWidget.map.map?.invalidateSize(); 123 this.mapWidget.map.map?.invalidateSize();
121 this.cd.detectChanges(); 124 this.cd.detectChanges();
@@ -140,32 +143,39 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy @@ -140,32 +143,39 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
140 this.currentTime = time; 143 this.currentTime = time;
141 const currentPosition = this.interpolatedTimeData 144 const currentPosition = this.interpolatedTimeData
142 .map(dataSource => dataSource[time]) 145 .map(dataSource => dataSource[time])
143 - .filter(ds => ds);  
144 - if (isUndefined(currentPosition[0])) {  
145 - const timePoints = Object.keys(this.interpolatedTimeData[0]).map(item => parseInt(item, 10));  
146 - for (let i = 1; i < timePoints.length; i++) {  
147 - if (timePoints[i - 1] < time && timePoints[i] > time) {  
148 - const beforePosition = this.interpolatedTimeData[0][timePoints[i - 1]];  
149 - const afterPosition = this.interpolatedTimeData[0][timePoints[i]];  
150 - const ratio = getRatio(timePoints[i - 1], timePoints[i], time);  
151 - currentPosition[0] = {  
152 - ...beforePosition,  
153 - time,  
154 - ...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio) 146 + for(let j = 0; j < this.interpolatedTimeData.length; j++) {
  147 + if (isUndefined(currentPosition[j])) {
  148 + const timePoints = Object.keys(this.interpolatedTimeData[j]).map(item => parseInt(item, 10));
  149 + for (let i = 1; i < timePoints.length; i++) {
  150 + if (timePoints[i - 1] < time && timePoints[i] > time) {
  151 + const beforePosition = this.interpolatedTimeData[j][timePoints[i - 1]];
  152 + const afterPosition = this.interpolatedTimeData[j][timePoints[i]];
  153 + const ratio = getRatio(timePoints[i - 1], timePoints[i], time);
  154 + currentPosition[j] = {
  155 + ...beforePosition,
  156 + time,
  157 + ...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio)
  158 + }
  159 + break;
155 } 160 }
156 - break;  
157 } 161 }
158 } 162 }
159 } 163 }
  164 + for(let j = 0; j < this.interpolatedTimeData.length; j++) {
  165 + if (isUndefined(currentPosition[j])) {
  166 + currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time);
  167 + }
  168 + }
160 this.calcLabel(); 169 this.calcLabel();
161 - this.calcTooltip(currentPosition.find(position => position.entityName === this.activeTrip.entityName)); 170 + this.calcMainTooltip(currentPosition);
162 if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { 171 if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) {
163 - this.mapWidget.map.updatePolylines(this.interpolatedTimeData.map(ds => _.values(ds)), true, this.activeTrip); 172 + const formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds));
  173 + this.mapWidget.map.updatePolylines(formattedInterpolatedTimeData, true);
164 if (this.settings.showPolygon) { 174 if (this.settings.showPolygon) {
165 this.mapWidget.map.updatePolygons(this.interpolatedTimeData); 175 this.mapWidget.map.updatePolygons(this.interpolatedTimeData);
166 } 176 }
167 if (this.settings.showPoints) { 177 if (this.settings.showPoints) {
168 - this.mapWidget.map.updatePoints(_.values(_.union(this.interpolatedTimeData)[0]), this.calcTooltip); 178 + this.mapWidget.map.updatePoints(formattedInterpolatedTimeData.map(ds => _.union(ds)), this.calcTooltip);
169 } 179 }
170 this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => { 180 this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => {
171 this.activeTrip = trip; 181 this.activeTrip = trip;
@@ -177,6 +187,23 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy @@ -177,6 +187,23 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
177 setActiveTrip() { 187 setActiveTrip() {
178 } 188 }
179 189
  190 + private calculateLastPoints(dataSource: dataMap, time: number): FormattedData {
  191 + const timeArr = Object.keys(dataSource);
  192 + let index = timeArr.findIndex((dtime, index) => {
  193 + return Number(dtime) >= time;
  194 + });
  195 +
  196 + if(index !== -1) {
  197 + if(Number(timeArr[index]) !== time && index !== 0) {
  198 + index--;
  199 + }
  200 + } else {
  201 + index = timeArr.length - 1;
  202 + }
  203 +
  204 + return dataSource[timeArr[index]];
  205 + }
  206 +
180 calculateIntervals() { 207 calculateIntervals() {
181 this.historicalData.forEach((dataSource, index) => { 208 this.historicalData.forEach((dataSource, index) => {
182 this.minTime = dataSource[0]?.time || Infinity; 209 this.minTime = dataSource[0]?.time || Infinity;
@@ -194,16 +221,19 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy @@ -194,16 +221,19 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
194 } 221 }
195 } 222 }
196 223
197 - calcTooltip = (point?: FormattedData): string => { 224 + calcTooltip = (point: FormattedData): string => {
198 const data = point ? point : this.activeTrip; 225 const data = point ? point : this.activeTrip;
199 const tooltipPattern: string = this.settings.useTooltipFunction ? 226 const tooltipPattern: string = this.settings.useTooltipFunction ?
200 safeExecute(this.settings.tooltipFunction, [data, this.historicalData, point.dsIndex]) : this.settings.tooltipPattern; 227 safeExecute(this.settings.tooltipFunction, [data, this.historicalData, point.dsIndex]) : this.settings.tooltipPattern;
201 - const tooltipText = parseWithTranslation.parseTemplate(tooltipPattern, data, true);  
202 - this.mainTooltip = this.sanitizer.sanitize(  
203 - SecurityContext.HTML, tooltipText);  
204 - this.cd.detectChanges();  
205 - this.activeTrip = point;  
206 - return tooltipText; 228 + return parseWithTranslation.parseTemplate(tooltipPattern, data, true);
  229 + }
  230 +
  231 + private calcMainTooltip(points: FormattedData[]): void {
  232 + const tooltips = [];
  233 + for (let point of points) {
  234 + tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point)));
  235 + }
  236 + this.mainTooltips = tooltips;
207 } 237 }
208 238
209 calcLabel() { 239 calcLabel() {
@@ -54,13 +54,13 @@ import { UtilsService } from '@core/services/utils.service'; @@ -54,13 +54,13 @@ import { UtilsService } from '@core/services/utils.service';
54 import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 54 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
55 import { TranslateService } from '@ngx-translate/core'; 55 import { TranslateService } from '@ngx-translate/core';
56 import { EntityType } from '@shared/models/entity-type.models'; 56 import { EntityType } from '@shared/models/entity-type.models';
57 -import { forkJoin, Observable, of, Subscription } from 'rxjs'; 57 +import { Observable, of, Subscription } from 'rxjs';
58 import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models'; 58 import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models';
59 import { 59 import {
60 EntityAliasDialogComponent, 60 EntityAliasDialogComponent,
61 EntityAliasDialogData 61 EntityAliasDialogData
62 } from '@home/components/alias/entity-alias-dialog.component'; 62 } from '@home/components/alias/entity-alias-dialog.component';
63 -import { catchError, map, mergeMap, tap } from 'rxjs/operators'; 63 +import { catchError, mergeMap, tap } from 'rxjs/operators';
64 import { MatDialog } from '@angular/material/dialog'; 64 import { MatDialog } from '@angular/material/dialog';
65 import { EntityService } from '@core/http/entity.service'; 65 import { EntityService } from '@core/http/entity.service';
66 import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; 66 import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
@@ -792,54 +792,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -792,54 +792,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
792 ); 792 );
793 } 793 }
794 794
795 - private fetchEntityKeys(entityAliasId: string, query: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {  
796 - return this.aliasController.resolveSingleEntityInfo(entityAliasId).pipe(  
797 - mergeMap((entity) => {  
798 - if (entity) {  
799 - const fetchEntityTasks: Array<Observable<Array<DataKey>>> = [];  
800 - for (const dataKeyType of dataKeyTypes) {  
801 - fetchEntityTasks.push(  
802 - this.entityService.getEntityKeys(  
803 - {entityType: entity.entityType, id: entity.id},  
804 - query,  
805 - dataKeyType,  
806 - {ignoreLoading: true, ignoreErrors: true}  
807 - ).pipe(  
808 - map((keys) => {  
809 - const dataKeys: Array<DataKey> = [];  
810 - for (const key of keys) {  
811 - dataKeys.push({name: key, type: dataKeyType});  
812 - }  
813 - return dataKeys;  
814 - }  
815 - ),  
816 - catchError(() => of([]))  
817 - ));  
818 - }  
819 - return forkJoin(fetchEntityTasks).pipe(  
820 - map(arrayOfDataKeys => {  
821 - const result = new Array<DataKey>();  
822 - arrayOfDataKeys.forEach((dataKeyArray) => {  
823 - result.push(...dataKeyArray);  
824 - });  
825 - return result;  
826 - }  
827 - ));  
828 - } else if (dataKeyTypes.includes(DataKeyType.alarm)) {  
829 - return this.entityService.getEntityKeys(null, query, DataKeyType.alarm).pipe(  
830 - map((keys) => {  
831 - const dataKeys: Array<DataKey> = [];  
832 - for (const key of keys) {  
833 - dataKeys.push({name: key, type: DataKeyType.alarm});  
834 - }  
835 - return dataKeys;  
836 - }  
837 - ),  
838 - catchError(() => of([]))  
839 - );  
840 - } else {  
841 - return of([]);  
842 - } 795 + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
  796 + return this.aliasController.getAliasInfo(entityAliasId).pipe(
  797 + mergeMap((aliasInfo) => {
  798 + return this.entityService.getEntityKeysByEntityFilter(
  799 + aliasInfo.entityFilter,
  800 + dataKeyTypes,
  801 + {ignoreLoading: true, ignoreErrors: true}
  802 + ).pipe(
  803 + catchError(() => of([]))
  804 + );
843 }), 805 }),
844 catchError(() => of([] as Array<DataKey>)) 806 catchError(() => of([] as Array<DataKey>))
845 ); 807 );
@@ -64,6 +64,12 @@ export interface EntityField { @@ -64,6 +64,12 @@ export interface EntityField {
64 time?: boolean; 64 time?: boolean;
65 } 65 }
66 66
  67 +export interface EntitiesKeysByQuery {
  68 + attribute: Array<string>;
  69 + timeseries: Array<string>;
  70 + entityTypes: EntityType[];
  71 +}
  72 +
67 export const entityFields: {[fieldName: string]: EntityField} = { 73 export const entityFields: {[fieldName: string]: EntityField} = {
68 createdTime: { 74 createdTime: {
69 keyName: 'createdTime', 75 keyName: 'createdTime',