Commit 45756dc7288ae1225a07fa53441a8ec1fa37279e

Authored by Igor Kulikov
Committed by GitHub
2 parents bb643df2 3aa97eec

Merge pull request #3834 from YevhenBondarenko/feature/keys-by-query

added ability to get attributes and timeseries keys by entity query
Showing 20 changed files with 349 additions and 78 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 }
@@ -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 }
@@ -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) {
@@ -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())
@@ -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 }
@@ -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);
@@ -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 {
@@ -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',