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 16 package org.thingsboard.server.controller;
17 17
18 18 import org.springframework.beans.factory.annotation.Autowired;
  19 +import org.springframework.http.ResponseEntity;
19 20 import org.springframework.security.access.prepost.PreAuthorize;
20 21 import org.springframework.web.bind.annotation.RequestBody;
21 22 import org.springframework.web.bind.annotation.RequestMapping;
22 23 import org.springframework.web.bind.annotation.RequestMethod;
  24 +import org.springframework.web.bind.annotation.RequestParam;
23 25 import org.springframework.web.bind.annotation.ResponseBody;
24 26 import org.springframework.web.bind.annotation.RestController;
  27 +import org.springframework.web.context.request.async.DeferredResult;
25 28 import org.thingsboard.server.common.data.exception.ThingsboardException;
  29 +import org.thingsboard.server.common.data.id.TenantId;
26 30 import org.thingsboard.server.common.data.page.PageData;
27 31 import org.thingsboard.server.common.data.query.AlarmData;
28 32 import org.thingsboard.server.common.data.query.AlarmDataQuery;
29 33 import org.thingsboard.server.common.data.query.EntityCountQuery;
30 34 import org.thingsboard.server.common.data.query.EntityData;
  35 +import org.thingsboard.server.common.data.query.EntityDataPageLink;
31 36 import org.thingsboard.server.common.data.query.EntityDataQuery;
32 37 import org.thingsboard.server.queue.util.TbCoreComponent;
33 38 import org.thingsboard.server.service.query.EntityQueryService;
... ... @@ -40,6 +45,7 @@ public class EntityQueryController extends BaseController {
40 45 @Autowired
41 46 private EntityQueryService entityQueryService;
42 47
  48 + private static final int MAX_PAGE_SIZE = 100;
43 49
44 50 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
45 51 @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST)
... ... @@ -76,4 +82,24 @@ public class EntityQueryController extends BaseController {
76 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 15 */
16 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 23 import lombok.extern.slf4j.Slf4j;
  24 +import org.checkerframework.checker.nullness.qual.Nullable;
19 25 import org.springframework.beans.factory.annotation.Autowired;
20 26 import org.springframework.beans.factory.annotation.Value;
  27 +import org.springframework.http.HttpStatus;
  28 +import org.springframework.http.ResponseEntity;
21 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 33 import org.thingsboard.server.common.data.id.EntityId;
  34 +import org.thingsboard.server.common.data.id.TenantId;
23 35 import org.thingsboard.server.common.data.page.PageData;
24 36 import org.thingsboard.server.common.data.query.AlarmData;
25 37 import org.thingsboard.server.common.data.query.AlarmDataQuery;
... ... @@ -31,12 +43,25 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder;
31 43 import org.thingsboard.server.common.data.query.EntityKey;
32 44 import org.thingsboard.server.common.data.query.EntityKeyType;
33 45 import org.thingsboard.server.dao.alarm.AlarmService;
  46 +import org.thingsboard.server.dao.attributes.AttributesService;
34 47 import org.thingsboard.server.dao.entity.EntityService;
35 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 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 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 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 66 @Service
42 67 @Slf4j
... ... @@ -52,6 +77,15 @@ public class DefaultEntityQueryService implements EntityQueryService {
52 77 @Value("${server.ws.max_entities_per_alarm_subscription:1000}")
53 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 89 @Override
56 90 public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) {
57 91 return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query);
... ... @@ -100,4 +134,103 @@ public class DefaultEntityQueryService implements EntityQueryService {
100 134 EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
101 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 15 */
16 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 21 import org.thingsboard.server.common.data.page.PageData;
19 22 import org.thingsboard.server.common.data.query.AlarmData;
20 23 import org.thingsboard.server.common.data.query.AlarmDataQuery;
... ... @@ -31,4 +34,7 @@ public interface EntityQueryService {
31 34
32 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 16 package org.thingsboard.server.dao.attributes;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import org.thingsboard.server.common.data.EntityType;
19 20 import org.thingsboard.server.common.data.id.DeviceProfileId;
20 21 import org.thingsboard.server.common.data.id.EntityId;
21 22 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -42,4 +43,6 @@ public interface AttributesService {
42 43
43 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 50 ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId);
51 51
52 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 16 package org.thingsboard.server.dao.attributes;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import org.thingsboard.server.common.data.EntityType;
19 20 import org.thingsboard.server.common.data.id.DeviceProfileId;
20 21 import org.thingsboard.server.common.data.id.EntityId;
21 22 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -41,4 +42,6 @@ public interface AttributesDao {
41 42 ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys);
42 43
43 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 20 import com.google.common.util.concurrent.ListenableFuture;
21 21 import org.springframework.beans.factory.annotation.Autowired;
22 22 import org.springframework.stereotype.Service;
  23 +import org.thingsboard.server.common.data.EntityType;
23 24 import org.thingsboard.server.common.data.id.DeviceProfileId;
24 25 import org.thingsboard.server.common.data.id.EntityId;
25 26 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -66,6 +67,11 @@ public class BaseAttributesService implements AttributesService {
66 67 }
67 68
68 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 75 public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
70 76 validate(entityId, scope);
71 77 attributes.forEach(attribute -> validate(attribute));
... ...
... ... @@ -56,5 +56,8 @@ public interface AttributeKvRepository extends CrudRepository<AttributeKvEntity,
56 56 "AND entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true)
57 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 22 import org.springframework.beans.factory.annotation.Autowired;
23 23 import org.springframework.beans.factory.annotation.Value;
24 24 import org.springframework.stereotype.Component;
  25 +import org.thingsboard.server.common.data.EntityType;
25 26 import org.thingsboard.server.common.data.id.DeviceProfileId;
26 27 import org.thingsboard.server.common.data.id.EntityId;
27 28 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -146,6 +147,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
146 147 }
147 148
148 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 156 public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) {
150 157 AttributeKvEntity entity = new AttributeKvEntity();
151 158 entity.setId(new AttributeKvCompositeKey(entityId.getEntityType(), entityId.getId(), attributeType, attribute.getKey()));
... ...
... ... @@ -61,6 +61,7 @@ import java.util.Optional;
61 61 import java.util.UUID;
62 62 import java.util.concurrent.ExecutionException;
63 63 import java.util.function.Function;
  64 +import java.util.stream.Collectors;
64 65
65 66 @Slf4j
66 67 @Component
... ... @@ -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 178 private ListenableFuture<Void> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
173 179 ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query);
174 180 return Futures.transformAsync(future, entryList -> {
... ...
... ... @@ -36,4 +36,9 @@ public interface TsKvLatestRepository extends CrudRepository<TsKvLatestEntity, T
36 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 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 122 }
123 123
124 124 @Override
  125 + public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
  126 + return timeseriesLatestDao.findAllKeysByEntityIds(tenantId, entityIds);
  127 + }
  128 +
  129 + @Override
125 130 public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
126 131 validate(entityId);
127 132 if (tsKvEntry == null) {
... ...
... ... @@ -87,6 +87,11 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
87 87 }
88 88
89 89 @Override
  90 + public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
  91 + return Collections.emptyList();
  92 + }
  93 +
  94 + @Override
90 95 public ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
91 96 BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind());
92 97 stmtBuilder.setString(0, entityId.getEntityType().name())
... ...
... ... @@ -35,4 +35,6 @@ public interface TimeseriesLatestDao {
35 35 ListenableFuture<Void> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
36 36
37 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 41 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
42 42 import { RuleChainService } from '@core/http/rule-chain.service';
43 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 45 import { UtilsService } from '@core/services/utils.service';
46 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 54 import { EntityRelationService } from '@core/http/entity-relation.service';
49 55 import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
50 56 import { Asset } from '@shared/models/asset.models';
... ... @@ -376,6 +382,13 @@ export class EntityService {
376 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 392 public findAlarmDataByQuery(query: AlarmDataQuery, config?: RequestConfig): Observable<PageData<AlarmData>> {
380 393 return this.http.post<PageData<AlarmData>>('/api/alarmsQuery/find', query, defaultHttpOptionsFromConfig(config));
381 394 }
... ... @@ -595,7 +608,7 @@ export class EntityService {
595 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 612 const entityFieldKeys: string[] = [entityFields.createdTime.keyName];
600 613 const query = searchText.toLowerCase();
601 614 switch (entityType) {
... ... @@ -637,7 +650,7 @@ export class EntityService {
637 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 654 const alarmKeys: string[] = Object.keys(alarmFields);
642 655 const query = searchText.toLowerCase();
643 656 return query ? alarmKeys.filter((alarmField) => alarmField.toLowerCase().indexOf(query) === 0) : alarmKeys;
... ... @@ -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 741 public createDatasourcesFromSubscriptionsInfo(subscriptionsInfo: Array<SubscriptionInfo>): Array<Datasource> {
676 742 const datasources = subscriptionsInfo.map(subscriptionInfo => this.createDatasourceFromSubscriptionInfo(subscriptionInfo));
677 743 this.utils.generateColors(datasources);
... ...
... ... @@ -36,7 +36,7 @@ import { EntityService } from '@core/http/entity.service';
36 36 import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models';
37 37 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
38 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 40 import { alarmFields } from '@shared/models/alarm.models';
41 41 import { JsFuncComponent } from '@shared/components/js-func.component';
42 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 95
96 96 filteredKeys: Observable<Array<string>>;
97 97 private latestKeySearchResult: Array<string> = null;
  98 + private fetchObservable$: Observable<Array<string>> = null;
98 99
99 100 keySearchText = '';
100 101
... ... @@ -205,31 +206,42 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
205 206 }
206 207
207 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 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 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 225 } else {
215 226 if (this.entityAliasId) {
216 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 229 } else {
219 230 fetchObservable = of([]);
220 231 }
221 232 }
222   - return fetchObservable.pipe(
  233 + this.fetchObservable$ = fetchObservable.pipe(
223 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 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 247 public validateOnSubmit() {
... ...
... ... @@ -20,5 +20,5 @@ import { Observable } from 'rxjs';
20 20
21 21 export interface DataKeysCallbacks {
22 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 38 Validators
39 39 } from '@angular/forms';
40 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 42 import { Store } from '@ngrx/store';
43 43 import { AppState } from '@app/core/core.state';
44 44 import { TranslateService } from '@ngx-translate/core';
... ... @@ -142,6 +142,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
142 142
143 143 searchText = '';
144 144 private latestSearchTextResult: Array<DataKey> = null;
  145 + private fetchObservable$: Observable<Array<DataKey>> = null;
145 146
146 147 private dirty = false;
147 148
... ... @@ -260,6 +261,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
260 261 if (!change.firstChange && change.currentValue !== change.previousValue) {
261 262 if (propName === 'entityAliasId') {
262 263 this.searchText = '';
  264 + this.fetchObservable$ = null;
263 265 this.latestSearchTextResult = null;
264 266 this.dirty = true;
265 267 } else if (['widgetType', 'datasourceType'].includes(propName)) {
... ... @@ -405,14 +407,24 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
405 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 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 425 if (this.datasourceType === DatasourceType.function) {
413   - const dataKeyFilter = this.createDataKeyFilter(this.searchText);
414 426 const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
415   - fetchObservable = of(targetKeysList.filter(dataKeyFilter));
  427 + fetchObservable = of(targetKeysList);
416 428 } else {
417 429 if (this.entityAliasId) {
418 430 const dataKeyTypes = [DataKeyType.timeseries];
... ... @@ -420,24 +432,25 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
420 432 dataKeyTypes.push(DataKeyType.attribute);
421 433 dataKeyTypes.push(DataKeyType.entityField);
422 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 439 } else {
428 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 451 private createDataKeyFilter(query: string): (key: DataKey) => boolean {
439 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 456 textIsNotEmpty(text: string): boolean {
... ...
... ... @@ -54,13 +54,13 @@ import { UtilsService } from '@core/services/utils.service';
54 54 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
55 55 import { TranslateService } from '@ngx-translate/core';
56 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 58 import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models';
59 59 import {
60 60 EntityAliasDialogComponent,
61 61 EntityAliasDialogData
62 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 64 import { MatDialog } from '@angular/material/dialog';
65 65 import { EntityService } from '@core/http/entity.service';
66 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 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 806 catchError(() => of([] as Array<DataKey>))
845 807 );
... ...
... ... @@ -64,6 +64,12 @@ export interface EntityField {
64 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 73 export const entityFields: {[fieldName: string]: EntityField} = {
68 74 createdTime: {
69 75 keyName: 'createdTime',
... ...