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 | 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}×eries=${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', | ... | ... |