Commit 45756dc7288ae1225a07fa53441a8ec1fa37279e
Committed by
GitHub
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}×eries=${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', |