Browse Source

Merge branch 'feature/entity-count-api'

pull/4190/head
Igor Kulikov 6 years ago
parent
commit
18d0d36976
  1. 54
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  2. 267
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  3. 315
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  5. 55
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java
  7. 42
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
  8. 3
      application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java
  10. 8
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java
  11. 33
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java
  12. 5
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java
  13. 21
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java
  14. 35
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java
  15. 25
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java
  16. 57
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java
  17. 8
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java
  19. 31
      application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
  20. 99
      application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
  21. 2
      application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
  22. 4
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java
  23. 4
      common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java
  24. 4
      common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java
  25. 9
      common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java
  26. 4
      common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java
  27. 14
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java
  28. 4
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java
  29. 1
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java
  30. 1
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java
  31. 30
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java
  32. 5
      common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java
  33. 2
      common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java
  34. 2
      common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java
  35. 10
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  36. 84
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  37. 25
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
  38. 54
      dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java
  39. 6
      dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java
  40. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java
  41. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
  42. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
  43. 1
      ui-ngx/package.json
  44. 21
      ui-ngx/src/app/core/api/alias-controller.ts
  45. 95
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  46. 7
      ui-ngx/src/app/core/api/entity-data.service.ts
  47. 15
      ui-ngx/src/app/core/http/entity.service.ts
  48. 4
      ui-ngx/src/app/core/services/item-buffer.service.ts
  49. 3
      ui-ngx/src/app/core/services/utils.service.ts
  50. 32
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  51. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  52. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  53. 4
      ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts
  54. 7
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  55. 5
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts
  56. 21
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  57. 10
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts
  58. 4
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html
  59. 9
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  60. 10
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  61. 59
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  62. 14
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts
  63. 11
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts
  64. 170
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  65. 40
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  66. 51
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  67. 1
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  68. 2
      ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts
  69. 12
      ui-ngx/src/app/shared/models/alias.models.ts
  70. 13
      ui-ngx/src/app/shared/models/query/query.models.ts
  71. 4
      ui-ngx/src/app/shared/models/relation.models.ts
  72. 111
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  73. 6
      ui-ngx/src/app/shared/models/widget.models.ts
  74. 6
      ui-ngx/src/app/shared/shared.module.ts
  75. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  76. 7
      ui-ngx/yarn.lock

54
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -54,6 +54,7 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
@ -92,7 +93,7 @@ import java.util.stream.Collectors;
public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService {
private static final int DEFAULT_LIMIT = 100;
private final Map<String, Map<Integer, TbAbstractDataSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
private final Map<String, Map<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
@Autowired
private TelemetryWebSocketService wsService;
@ -202,7 +203,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
//TODO: validate number of dynamic page links against rate limits. Ignore dynamic flag if limit is reached.
TbEntityDataSubCtx finalCtx = ctx;
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(
() -> refreshDynamicQuery(tenantId, customerId, finalCtx),
() -> refreshDynamicQuery(finalCtx),
dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
finalCtx.setRefreshTask(task);
}
@ -235,6 +236,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}, wsCallBackExecutor);
}
@Override
public void handleCmd(TelemetryWebSocketSessionRef session, EntityCountCmd cmd) {
TbEntityCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
if (ctx == null) {
ctx = createSubCtx(session, cmd);
long start = System.currentTimeMillis();
ctx.fetchData();
long end = System.currentTimeMillis();
stats.getRegularQueryInvocationCnt().incrementAndGet();
stats.getRegularQueryTimeSpent().addAndGet(end - start);
TbEntityCountSubCtx finalCtx = ctx;
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(
() -> refreshDynamicQuery(finalCtx),
dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
finalCtx.setRefreshTask(task);
} else {
log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd);
}
}
@Override
public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) {
TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
@ -267,7 +288,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
}
private void refreshDynamicQuery(TenantId tenantId, CustomerId customerId, TbEntityDataSubCtx finalCtx) {
private void refreshDynamicQuery(TbAbstractSubCtx finalCtx) {
try {
long start = System.currentTimeMillis();
finalCtx.update();
@ -299,7 +320,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
if (cmd.getQuery() != null) {
@ -309,8 +330,20 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
return ctx;
}
private TbEntityCountSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId());
if (cmd.getQuery() != null) {
ctx.setAndResolveQuery(cmd.getQuery());
}
sessionSubs.put(cmd.getCmdId(), ctx);
return ctx;
}
private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription);
ctx.setAndResolveQuery(cmd.getQuery());
@ -319,8 +352,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
@SuppressWarnings("unchecked")
private <T extends TbAbstractDataSubCtx> T getSubCtx(String sessionId, int cmdId) {
Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.get(sessionId);
private <T extends TbAbstractSubCtx> T getSubCtx(String sessionId, int cmdId) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.get(sessionId);
if (sessionSubs != null) {
return (T) sessionSubs.get(cmdId);
} else {
@ -464,17 +497,16 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
cleanupAndCancel(getSubCtx(sessionId, cmd.getCmdId()));
}
private void cleanupAndCancel(TbAbstractDataSubCtx ctx) {
private void cleanupAndCancel(TbAbstractSubCtx ctx) {
if (ctx != null) {
ctx.cancelTasks();
ctx.clearEntitySubscriptions();
ctx.clearDynamicValueSubscriptions();
ctx.clearSubscriptions();
}
}
@Override
public void cancelAllSessionSubscriptions(String sessionId) {
Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.remove(sessionId);
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.remove(sessionId);
if (sessionSubs != null) {
sessionSubs.values().forEach(this::cleanupAndCancel);
}

267
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java

@ -15,32 +15,16 @@
*/
package org.thingsboard.server.service.subscription;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AbstractDataQuery;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateType;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
@ -52,140 +36,25 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Data
public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends EntityDataPageLink>> {
public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends EntityDataPageLink>> extends TbAbstractSubCtx<T> {
protected final String serviceId;
protected final SubscriptionServiceStatistics stats;
protected final TelemetryWebSocketService wsService;
protected final EntityService entityService;
protected final TbLocalSubscriptionService localSubscriptionService;
protected final AttributesService attributesService;
protected final TelemetryWebSocketSessionRef sessionRef;
protected final int cmdId;
protected final Map<Integer, EntityId> subToEntityIdMap;
protected final Set<Integer> subToDynamicValueKeySet;
@Getter
protected final Map<DynamicValueKey, List<DynamicValue>> dynamicValues;
@Getter
protected PageData<EntityData> data;
@Getter
@Setter
protected T query;
@Setter
protected volatile ScheduledFuture<?> refreshTask;
public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats,
TelemetryWebSocketSessionRef sessionRef, int cmdId) {
this.serviceId = serviceId;
this.wsService = wsService;
this.entityService = entityService;
this.localSubscriptionService = localSubscriptionService;
this.attributesService = attributesService;
this.stats = stats;
this.sessionRef = sessionRef;
this.cmdId = cmdId;
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
this.subToEntityIdMap = new ConcurrentHashMap<>();
this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet();
this.dynamicValues = new ConcurrentHashMap<>();
}
public void setAndResolveQuery(T query) {
dynamicValues.clear();
this.query = query;
if (query != null && query.getKeyFilters() != null) {
for (KeyFilter filter : query.getKeyFilters()) {
registerDynamicValues(filter.getPredicate());
}
}
resolve(getTenantId(), getCustomerId(), getUserId());
}
public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) {
List<ListenableFuture<DynamicValueKeySub>> futures = new ArrayList<>();
for (DynamicValueKey key : dynamicValues.keySet()) {
switch (key.getSourceType()) {
case CURRENT_TENANT:
futures.add(resolveEntityValue(tenantId, tenantId, key));
break;
case CURRENT_CUSTOMER:
if (customerId != null && !customerId.isNullUid()) {
futures.add(resolveEntityValue(tenantId, customerId, key));
}
break;
case CURRENT_USER:
if (userId != null && !userId.isNullUid()) {
futures.add(resolveEntityValue(tenantId, userId, key));
}
break;
}
}
try {
Map<EntityId, Map<String, DynamicValueKeySub>> tmpSubMap = new HashMap<>();
for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) {
tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub);
}
for (EntityId entityId : tmpSubMap.keySet()) {
Map<String, Long> keyStates = new HashMap<>();
Map<String, DynamicValueKeySub> dynamicValueKeySubMap = tmpSubMap.get(entityId);
dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs()));
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionRef.getSessionId())
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap))
.allKeys(false)
.keyStates(keyStates)
.scope(TbAttributeSubscriptionScope.SERVER_SCOPE)
.build();
subToDynamicValueKeySet.add(subIdx);
localSubscriptionService.addSubscription(sub);
}
} catch (InterruptedException | ExecutionException e) {
log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());
}
}
private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate,
Map<String, DynamicValueKeySub> dynamicValueKeySubMap) {
Map<String, TsValue> latestUpdate = new HashMap<>();
subscriptionUpdate.getData().forEach((k, v) -> {
Object[] data = (Object[]) v.get(0);
latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
});
boolean invalidateFilter = false;
for (Map.Entry<String, TsValue> entry : latestUpdate.entrySet()) {
String k = entry.getKey();
TsValue tsValue = entry.getValue();
DynamicValueKeySub sub = dynamicValueKeySubMap.get(k);
if (sub.updateValue(tsValue)) {
invalidateFilter = true;
updateDynamicValuesByKey(sub, tsValue);
}
}
if (invalidateFilter) {
update();
}
}
public void fetchData() {
@ -231,104 +100,10 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
return data.getData();
}
@Data
private static class DynamicValueKeySub {
private final DynamicValueKey key;
private final EntityId entityId;
private long lastUpdateTs;
private String lastUpdateValue;
boolean updateValue(TsValue value) {
if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) {
this.lastUpdateTs = value.getTs();
this.lastUpdateValue = value.getValue();
return true;
} else {
return false;
}
}
}
private ListenableFuture<DynamicValueKeySub> resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) {
ListenableFuture<Optional<AttributeKvEntry>> entry = attributesService.find(tenantId, entityId,
TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute());
return Futures.transform(entry, attributeOpt -> {
DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId);
if (attributeOpt.isPresent()) {
AttributeKvEntry attribute = attributeOpt.get();
sub.setLastUpdateTs(attribute.getLastUpdateTs());
sub.setLastUpdateValue(attribute.getValueAsString());
updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString()));
}
return sub;
}, MoreExecutors.directExecutor());
}
@SuppressWarnings("unchecked")
private void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) {
DynamicValueKey dvk = sub.getKey();
switch (dvk.getPredicateType()) {
case STRING:
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue()));
break;
case NUMERIC:
try {
Double dValue = Double.parseDouble(tsValue.getValue());
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue));
} catch (NumberFormatException e) {
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null));
}
break;
case BOOLEAN:
Boolean bValue = Boolean.parseBoolean(tsValue.getValue());
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue));
break;
}
}
@SuppressWarnings("unchecked")
private void registerDynamicValues(KeyFilterPredicate predicate) {
switch (predicate.getType()) {
case STRING:
case NUMERIC:
case BOOLEAN:
Optional<DynamicValue> value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate);
if (value.isPresent()) {
DynamicValue dynamicValue = value.get();
DynamicValueKey key = new DynamicValueKey(
predicate.getType(),
dynamicValue.getSourceType(),
dynamicValue.getSourceAttribute());
dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue);
}
break;
case COMPLEX:
((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues);
}
}
private Optional<DynamicValue<T>> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate<T> predicate) {
if (predicate.getValue().getUserValue() == null) {
return Optional.ofNullable(predicate.getValue().getDynamicValue());
} else {
return Optional.empty();
}
}
public String getSessionId() {
return sessionRef.getSessionId();
}
public TenantId getTenantId() {
return sessionRef.getSecurityCtx().getTenantId();
}
public CustomerId getCustomerId() {
return sessionRef.getSecurityCtx().getCustomerId();
}
public UserId getUserId() {
return sessionRef.getSecurityCtx().getId();
@Override
public void clearSubscriptions() {
clearEntitySubscriptions();
super.clearSubscriptions();
}
public void clearEntitySubscriptions() {
@ -340,26 +115,6 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
}
}
public void clearDynamicValueSubscriptions() {
if (subToDynamicValueKeySet != null) {
for (Integer subId : subToDynamicValueKeySet) {
localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId);
}
subToDynamicValueKeySet.clear();
}
}
public void setRefreshTask(ScheduledFuture<?> task) {
this.refreshTask = task;
}
public void cancelTasks() {
if (this.refreshTask != null) {
log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId);
this.refreshTask.cancel(true);
}
}
public void createSubscriptions(List<EntityKey> keys, boolean resultToLatestValues) {
Map<EntityKeyType, List<EntityKey>> keysByType = getEntityKeyByTypeMap(keys);
for (EntityData entityData : data.getData()) {
@ -459,14 +214,4 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
abstract void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues);
@Data
private static class DynamicValueKey {
@Getter
private final FilterPredicateType predicateType;
@Getter
private final DynamicValueSourceType sourceType;
@Getter
private final String sourceAttribute;
}
}

315
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java

@ -0,0 +1,315 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.subscription;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateType;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
@Slf4j
@Data
public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
protected final String serviceId;
protected final SubscriptionServiceStatistics stats;
protected final TelemetryWebSocketService wsService;
protected final EntityService entityService;
protected final TbLocalSubscriptionService localSubscriptionService;
protected final AttributesService attributesService;
protected final TelemetryWebSocketSessionRef sessionRef;
protected final int cmdId;
protected final Set<Integer> subToDynamicValueKeySet;
@Getter
protected final Map<DynamicValueKey, List<DynamicValue>> dynamicValues;
@Getter
@Setter
protected T query;
@Setter
protected volatile ScheduledFuture<?> refreshTask;
public TbAbstractSubCtx(String serviceId, TelemetryWebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats,
TelemetryWebSocketSessionRef sessionRef, int cmdId) {
this.serviceId = serviceId;
this.wsService = wsService;
this.entityService = entityService;
this.localSubscriptionService = localSubscriptionService;
this.attributesService = attributesService;
this.stats = stats;
this.sessionRef = sessionRef;
this.cmdId = cmdId;
this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet();
this.dynamicValues = new ConcurrentHashMap<>();
}
public void setAndResolveQuery(T query) {
dynamicValues.clear();
this.query = query;
if (query != null && query.getKeyFilters() != null) {
for (KeyFilter filter : query.getKeyFilters()) {
registerDynamicValues(filter.getPredicate());
}
}
resolve(getTenantId(), getCustomerId(), getUserId());
}
public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) {
List<ListenableFuture<DynamicValueKeySub>> futures = new ArrayList<>();
for (DynamicValueKey key : dynamicValues.keySet()) {
switch (key.getSourceType()) {
case CURRENT_TENANT:
futures.add(resolveEntityValue(tenantId, tenantId, key));
break;
case CURRENT_CUSTOMER:
if (customerId != null && !customerId.isNullUid()) {
futures.add(resolveEntityValue(tenantId, customerId, key));
}
break;
case CURRENT_USER:
if (userId != null && !userId.isNullUid()) {
futures.add(resolveEntityValue(tenantId, userId, key));
}
break;
}
}
try {
Map<EntityId, Map<String, DynamicValueKeySub>> tmpSubMap = new HashMap<>();
for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) {
tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub);
}
for (EntityId entityId : tmpSubMap.keySet()) {
Map<String, Long> keyStates = new HashMap<>();
Map<String, DynamicValueKeySub> dynamicValueKeySubMap = tmpSubMap.get(entityId);
dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs()));
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionRef.getSessionId())
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap))
.allKeys(false)
.keyStates(keyStates)
.scope(TbAttributeSubscriptionScope.SERVER_SCOPE)
.build();
subToDynamicValueKeySet.add(subIdx);
localSubscriptionService.addSubscription(sub);
}
} catch (InterruptedException | ExecutionException e) {
log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());
}
}
private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate,
Map<String, DynamicValueKeySub> dynamicValueKeySubMap) {
Map<String, TsValue> latestUpdate = new HashMap<>();
subscriptionUpdate.getData().forEach((k, v) -> {
Object[] data = (Object[]) v.get(0);
latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
});
boolean invalidateFilter = false;
for (Map.Entry<String, TsValue> entry : latestUpdate.entrySet()) {
String k = entry.getKey();
TsValue tsValue = entry.getValue();
DynamicValueKeySub sub = dynamicValueKeySubMap.get(k);
if (sub.updateValue(tsValue)) {
invalidateFilter = true;
updateDynamicValuesByKey(sub, tsValue);
}
}
if (invalidateFilter) {
update();
}
}
public abstract void fetchData();
protected abstract void update();
public void clearSubscriptions() {
clearDynamicValueSubscriptions();
}
@Data
private static class DynamicValueKeySub {
private final DynamicValueKey key;
private final EntityId entityId;
private long lastUpdateTs;
private String lastUpdateValue;
boolean updateValue(TsValue value) {
if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) {
this.lastUpdateTs = value.getTs();
this.lastUpdateValue = value.getValue();
return true;
} else {
return false;
}
}
}
private ListenableFuture<DynamicValueKeySub> resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) {
ListenableFuture<Optional<AttributeKvEntry>> entry = attributesService.find(tenantId, entityId,
TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute());
return Futures.transform(entry, attributeOpt -> {
DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId);
if (attributeOpt.isPresent()) {
AttributeKvEntry attribute = attributeOpt.get();
sub.setLastUpdateTs(attribute.getLastUpdateTs());
sub.setLastUpdateValue(attribute.getValueAsString());
updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString()));
}
return sub;
}, MoreExecutors.directExecutor());
}
@SuppressWarnings("unchecked")
protected void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) {
DynamicValueKey dvk = sub.getKey();
switch (dvk.getPredicateType()) {
case STRING:
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue()));
break;
case NUMERIC:
try {
Double dValue = Double.parseDouble(tsValue.getValue());
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue));
} catch (NumberFormatException e) {
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null));
}
break;
case BOOLEAN:
Boolean bValue = Boolean.parseBoolean(tsValue.getValue());
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue));
break;
}
}
@SuppressWarnings("unchecked")
private void registerDynamicValues(KeyFilterPredicate predicate) {
switch (predicate.getType()) {
case STRING:
case NUMERIC:
case BOOLEAN:
Optional<DynamicValue> value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate);
if (value.isPresent()) {
DynamicValue dynamicValue = value.get();
DynamicValueKey key = new DynamicValueKey(
predicate.getType(),
dynamicValue.getSourceType(),
dynamicValue.getSourceAttribute());
dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue);
}
break;
case COMPLEX:
((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues);
}
}
private Optional<DynamicValue<T>> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate<T> predicate) {
if (predicate.getValue().getUserValue() == null) {
return Optional.ofNullable(predicate.getValue().getDynamicValue());
} else {
return Optional.empty();
}
}
public String getSessionId() {
return sessionRef.getSessionId();
}
public TenantId getTenantId() {
return sessionRef.getSecurityCtx().getTenantId();
}
public CustomerId getCustomerId() {
return sessionRef.getSecurityCtx().getCustomerId();
}
public UserId getUserId() {
return sessionRef.getSecurityCtx().getId();
}
protected void clearDynamicValueSubscriptions() {
if (subToDynamicValueKeySet != null) {
for (Integer subId : subToDynamicValueKeySet) {
localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId);
}
subToDynamicValueKeySet.clear();
}
}
public void setRefreshTask(ScheduledFuture<?> task) {
this.refreshTask = task;
}
public void cancelTasks() {
if (this.refreshTask != null) {
log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId);
this.refreshTask.cancel(true);
}
}
@Data
public static class DynamicValueKey {
@Getter
private final FilterPredicateType predicateType;
@Getter
private final DynamicValueSourceType sourceType;
@Getter
private final String sourceAttribute;
}
}

3
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java

@ -90,8 +90,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
AlarmDataUpdate update;
if (!entitiesMap.isEmpty()) {
long start = System.currentTimeMillis();
PageData<AlarmData> alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(),
query, getOrderedEntityIds());
PageData<AlarmData> alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(), query, getOrderedEntityIds());
long end = System.currentTimeMillis();
stats.getAlarmQueryInvocationCnt().incrementAndGet();
stats.getAlarmQueryTimeSpent().addAndGet(end - start);

55
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.subscription;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
@Slf4j
public class TbEntityCountSubCtx extends TbAbstractSubCtx<EntityCountQuery> {
private volatile int result;
public TbEntityCountSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService,
TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService,
SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
}
@Override
public void fetchData() {
result = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query);
wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result));
}
@Override
protected void update() {
int newCount = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query);
if (newCount != result) {
result = newCount;
wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result));
}
}
}

3
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.subscription;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd;
@ -25,6 +26,8 @@ public interface TbEntityDataSubscriptionService {
void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd);
void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityCountCmd cmd);
void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd);
void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId);

42
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java

@ -51,22 +51,22 @@ import org.thingsboard.server.service.security.ValidationResult;
import org.thingsboard.server.service.security.ValidationResultCode;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.subscription.TbAttributeSubscription;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import org.thingsboard.server.service.subscription.TbAttributeSubscription;
import org.thingsboard.server.service.subscription.TbTimeseriesSubscription;
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd;
import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd;
import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd;
import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd;
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd;
import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
@ -216,12 +216,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
if (cmdsWrapper.getAlarmDataCmds() != null) {
cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityCountCmds() != null) {
cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) {
cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) {
cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) {
cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
}
} catch (IOException e) {
log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
@ -239,6 +245,16 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
private void handleWsEntityCountCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd);
}
}
private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
@ -264,7 +280,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
@Override
public void sendWsMsg(String sessionId, DataUpdate update) {
public void sendWsMsg(String sessionId, CmdUpdate update) {
sendWsMsg(sessionId, update.getCmdId(), update);
}
@ -679,6 +695,20 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return true;
}
private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
sendWsMsg(sessionRef, update);
return false;
} else if (cmd.getQuery() == null) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!");
sendWsMsg(sessionRef, update);
return false;
}
return true;
}
private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,

3
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.telemetry;
import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
@ -29,6 +30,6 @@ public interface TelemetryWebSocketService {
void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update);
void sendWsMsg(String sessionId, DataUpdate update);
void sendWsMsg(String sessionId, CmdUpdate update);
}

6
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java

@ -21,6 +21,8 @@ import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd;
import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUnsubscribeCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
@ -46,4 +48,8 @@ public class TelemetryPluginCmdsWrapper {
private List<AlarmDataUnsubscribeCmd> alarmDataUnsubscribeCmds;
private List<EntityCountCmd> entityCountCmds;
private List<EntityCountUnsubscribeCmd> entityCountUnsubscribeCmds;
}

8
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java

@ -18,14 +18,14 @@ package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
import java.util.List;
@ToString
public class AlarmDataUpdate extends DataUpdate<AlarmData> {
@Getter
@ -44,8 +44,8 @@ public class AlarmDataUpdate extends DataUpdate<AlarmData> {
}
@Override
public DataUpdateType getDataUpdateType() {
return DataUpdateType.ALARM_DATA;
public CmdUpdateType getCmdUpdateType() {
return CmdUpdateType.ALARM_DATA;
}
@JsonCreator

33
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class CmdUpdate {
private final int cmdId;
private final int errorCode;
private final String errorMsg;
public abstract CmdUpdateType getCmdUpdateType();
}

5
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java → application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java

@ -15,7 +15,8 @@
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
public enum DataUpdateType {
public enum CmdUpdateType {
ENTITY_DATA,
ALARM_DATA
ALARM_DATA,
COUNT_DATA
}

21
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java

@ -15,24 +15,24 @@
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
import java.util.List;
@Data
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class DataUpdate<T> {
public abstract class DataUpdate<T> extends CmdUpdate {
private final int cmdId;
@Getter
private final PageData<T> data;
@Getter
private final List<T> update;
private final int errorCode;
private final String errorMsg;
public DataUpdate(int cmdId, PageData<T> data, List<T> update, int errorCode, String errorMsg) {
super(cmdId, errorCode, errorMsg);
this.data = data;
this.update = update;
}
public DataUpdate(int cmdId, PageData<T> data, List<T> update) {
this(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null);
@ -42,5 +42,4 @@ public abstract class DataUpdate<T> {
this(cmdId, null, null, errorCode, errorMsg);
}
public abstract DataUpdateType getDataUpdateType();
}

35
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
public class EntityCountCmd extends DataCmd {
@Getter
private final EntityCountQuery query;
@JsonCreator
public EntityCountCmd(@JsonProperty("cmdId") int cmdId,
@JsonProperty("query") EntityCountQuery query) {
super(cmdId);
this.query = query;
}
}

25
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import lombok.Data;
@Data
public class EntityCountUnsubscribeCmd implements UnsubscribeCmd {
private final int cmdId;
}

57
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
import java.util.List;
@ToString
public class EntityCountUpdate extends CmdUpdate {
@Getter
private int count;
public EntityCountUpdate(int cmdId, int count) {
super(cmdId, SubscriptionErrorCode.NO_ERROR.getCode(), null);
this.count = count;
}
public EntityCountUpdate(int cmdId, int errorCode, String errorMsg) {
super(cmdId, errorCode, errorMsg);
}
@Override
public CmdUpdateType getCmdUpdateType() {
return CmdUpdateType.COUNT_DATA;
}
@JsonCreator
public EntityCountUpdate(@JsonProperty("cmdId") int cmdId,
@JsonProperty("count") int count,
@JsonProperty("errorCode") int errorCode,
@JsonProperty("errorMsg") String errorMsg) {
super(cmdId, errorCode, errorMsg);
this.count = count;
}
}

8
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java

@ -16,16 +16,16 @@
package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
import java.util.List;
@ToString
public class EntityDataUpdate extends DataUpdate<EntityData> {
@Getter
@ -41,8 +41,8 @@ public class EntityDataUpdate extends DataUpdate<EntityData> {
}
@Override
public DataUpdateType getDataUpdateType() {
return DataUpdateType.ENTITY_DATA;
public CmdUpdateType getCmdUpdateType() {
return CmdUpdateType.ENTITY_DATA;
}
@JsonCreator

2
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java

@ -15,8 +15,6 @@
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import lombok.Data;
public interface UnsubscribeCmd {
int getCmdId();

31
application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
@ -132,6 +133,14 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
EntityTypeFilter filter2 = new EntityTypeFilter();
filter2.setEntityType(EntityType.DEVICE);
EntityCountQuery countQuery2 = new EntityCountQuery(filter2);
Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class);
Assert.assertEquals(97, count2.longValue());
}
@Test
@ -198,11 +207,31 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
Assert.assertEquals(11, data.getTotalElements());
Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue());
EntityTypeFilter filter2 = new EntityTypeFilter();
filter2.setEntityType(EntityType.DEVICE);
EntityDataSortOrder sortOrder2 = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink2 = new EntityDataPageLink(10, 0, null, sortOrder2);
List<EntityKey> entityFields2 = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
EntityDataQuery query2 = new EntityDataQuery(filter2, pageLink2, entityFields2, null, null);
PageData<EntityData> data2 =
doPostWithTypedResponse("/api/entitiesQuery/find", query2, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(97, data2.getTotalElements());
Assert.assertEquals(10, data2.getTotalPages());
Assert.assertTrue(data2.hasNext());
Assert.assertEquals(10, data2.getData().size());
}
@Test
public void testFindEntityDataByQueryWithAttributes() throws Exception {
List<Device> devices = new ArrayList<>();
List<Long> temperatures = new ArrayList<>();
List<Long> highTemperatures = new ArrayList<>();

99
application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java

@ -35,16 +35,23 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
@ -243,6 +250,98 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]);
}
@Test
public void testEntityCountWsCmd() throws Exception {
Device device = new Device();
device.setName("Device");
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
device = doPost("/api/device", device, Device.class);
AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 42L));
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Collections.singletonList(dataPoint1));
DeviceTypeFilter dtf1 = new DeviceTypeFilter();
dtf1.setDeviceNameFilter("D");
dtf1.setDeviceType("default");
EntityCountQuery edq1 = new EntityCountQuery(dtf1, Collections.emptyList());
EntityCountCmd cmd1 = new EntityCountCmd(1, edq1);
TelemetryPluginCmdsWrapper wrapper1 = new TelemetryPluginCmdsWrapper();
wrapper1.setEntityCountCmds(Collections.singletonList(cmd1));
wsClient.send(mapper.writeValueAsString(wrapper1));
String msg1 = wsClient.waitForReply();
EntityCountUpdate update1 = mapper.readValue(msg1, EntityCountUpdate.class);
Assert.assertEquals(1, update1.getCmdId());
Assert.assertEquals(1, update1.getCount());
DeviceTypeFilter dtf2 = new DeviceTypeFilter();
dtf2.setDeviceNameFilter("D");
dtf2.setDeviceType("non-existing-device-type");
EntityCountQuery edq2 = new EntityCountQuery(dtf2, Collections.emptyList());
EntityCountCmd cmd2 = new EntityCountCmd(2, edq2);
TelemetryPluginCmdsWrapper wrapper2 = new TelemetryPluginCmdsWrapper();
wrapper2.setEntityCountCmds(Collections.singletonList(cmd2));
wsClient.send(mapper.writeValueAsString(wrapper2));
String msg2 = wsClient.waitForReply();
EntityCountUpdate update2 = mapper.readValue(msg2, EntityCountUpdate.class);
Assert.assertEquals(2, update2.getCmdId());
Assert.assertEquals(0, update2.getCount());
KeyFilter highTemperatureFilter = new KeyFilter();
highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
NumericFilterPredicate predicate = new NumericFilterPredicate();
predicate.setValue(FilterPredicateValue.fromDouble(40));
predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
highTemperatureFilter.setPredicate(predicate);
highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
DeviceTypeFilter dtf3 = new DeviceTypeFilter();
dtf3.setDeviceNameFilter("D");
dtf3.setDeviceType("default");
EntityCountQuery edq3 = new EntityCountQuery(dtf3, Collections.singletonList(highTemperatureFilter));
EntityCountCmd cmd3 = new EntityCountCmd(3, edq3);
TelemetryPluginCmdsWrapper wrapper3 = new TelemetryPluginCmdsWrapper();
wrapper3.setEntityCountCmds(Collections.singletonList(cmd3));
wsClient.send(mapper.writeValueAsString(wrapper3));
String msg3 = wsClient.waitForReply();
EntityCountUpdate update3 = mapper.readValue(msg3, EntityCountUpdate.class);
Assert.assertEquals(3, update3.getCmdId());
Assert.assertEquals(1, update3.getCount());
KeyFilter highTemperatureFilter2 = new KeyFilter();
highTemperatureFilter2.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
NumericFilterPredicate predicate2 = new NumericFilterPredicate();
predicate2.setValue(FilterPredicateValue.fromDouble(50));
predicate2.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
highTemperatureFilter2.setPredicate(predicate2);
highTemperatureFilter2.setValueType(EntityKeyValueType.NUMERIC);
DeviceTypeFilter dtf4 = new DeviceTypeFilter();
dtf4.setDeviceNameFilter("D");
dtf4.setDeviceType("default");
EntityCountQuery edq4 = new EntityCountQuery(dtf4, Collections.singletonList(highTemperatureFilter2));
EntityCountCmd cmd4 = new EntityCountCmd(4, edq4);
TelemetryPluginCmdsWrapper wrapper4 = new TelemetryPluginCmdsWrapper();
wrapper4.setEntityCountCmds(Collections.singletonList(cmd4));
wsClient.send(mapper.writeValueAsString(wrapper4));
String msg4 = wsClient.waitForReply();
EntityCountUpdate update4 = mapper.readValue(msg4, EntityCountUpdate.class);
Assert.assertEquals(4, update4.getCmdId());
Assert.assertEquals(0, update4.getCount());
}
@Test
public void testEntityDataLatestWidgetFlow() throws Exception {
Device device = new Device();

2
application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java

@ -27,7 +27,7 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest",
// "org.thingsboard.server.controller.sql.TenantProfileControllerSqlTest",
// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest",
"org.thingsboard.server.controller.sql.*Test",
})
public class ControllerSqlTestSuite {

4
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java

@ -19,7 +19,7 @@ import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.Collections;
@ -39,7 +39,7 @@ public class AssetSearchQuery {
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(parameters);
query.setFilters(
Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
Collections.singletonList(EntityType.ASSET))));
return query;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java

@ -19,7 +19,7 @@ import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.Collections;
@ -36,7 +36,7 @@ public class DeviceSearchQuery {
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(parameters);
query.setFilters(
Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
Collections.singletonList(EntityType.DEVICE))));
return query;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java

@ -19,7 +19,7 @@ import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.Collections;
@ -36,7 +36,7 @@ public class EntityViewSearchQuery {
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(parameters);
query.setFilters(
Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
Collections.singletonList(EntityType.ENTITY_VIEW))));
return query;
}

9
common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java

@ -29,15 +29,13 @@ public abstract class AbstractDataQuery<T extends EntityDataPageLink> extends En
protected List<EntityKey> entityFields;
@Getter
protected List<EntityKey> latestValues;
@Getter
protected List<KeyFilter> keyFilters;
public AbstractDataQuery() {
super();
}
public AbstractDataQuery(EntityFilter entityFilter) {
super(entityFilter);
public AbstractDataQuery(EntityFilter entityFilter, List<KeyFilter> keyFilters) {
super(entityFilter, keyFilters);
}
public AbstractDataQuery(EntityFilter entityFilter,
@ -45,11 +43,10 @@ public abstract class AbstractDataQuery<T extends EntityDataPageLink> extends En
List<EntityKey> entityFields,
List<EntityKey> latestValues,
List<KeyFilter> keyFilters) {
super(entityFilter);
super(entityFilter, keyFilters);
this.pageLink = pageLink;
this.entityFields = entityFields;
this.latestValues = latestValues;
this.keyFilters = keyFilters;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java

@ -30,8 +30,8 @@ public class AlarmDataQuery extends AbstractDataQuery<AlarmDataPageLink> {
public AlarmDataQuery() {
}
public AlarmDataQuery(EntityFilter entityFilter) {
super(entityFilter);
public AlarmDataQuery(EntityFilter entityFilter, List<KeyFilter> keyFilters) {
super(entityFilter, keyFilters);
}
public AlarmDataQuery(EntityFilter entityFilter, AlarmDataPageLink pageLink, List<EntityKey> entityFields, List<EntityKey> latestValues, List<KeyFilter> keyFilters, List<EntityKey> alarmFields) {

14
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java

@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.query;
import lombok.Getter;
import java.util.Collections;
import java.util.List;
public class EntityCountQuery {
@Getter
private EntityFilter entityFilter;
public EntityCountQuery() {}
@Getter
protected List<KeyFilter> keyFilters;
public EntityCountQuery() {
}
public EntityCountQuery(EntityFilter entityFilter) {
this(entityFilter, Collections.emptyList());
}
public EntityCountQuery(EntityFilter entityFilter, List<KeyFilter> keyFilters) {
this.entityFilter = entityFilter;
this.keyFilters = keyFilters;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java

@ -27,8 +27,8 @@ public class EntityDataQuery extends AbstractDataQuery<EntityDataPageLink> {
public EntityDataQuery() {
}
public EntityDataQuery(EntityFilter entityFilter) {
super(entityFilter);
public EntityDataQuery(EntityFilter entityFilter, List<KeyFilter> keyFilters) {
super(entityFilter, keyFilters);
}
public EntityDataQuery(EntityFilter entityFilter, EntityDataPageLink pageLink, List<EntityKey> entityFields, List<EntityKey> latestValues, List<KeyFilter> keyFilters) {

1
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java

@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonSubTypes.Type(value = SingleEntityFilter.class, name = "singleEntity"),
@JsonSubTypes.Type(value = EntityListFilter.class, name = "entityList"),
@JsonSubTypes.Type(value = EntityNameFilter.class, name = "entityName"),
@JsonSubTypes.Type(value = EntityTypeFilter.class, name = "entityType"),
@JsonSubTypes.Type(value = AssetTypeFilter.class, name = "assetType"),
@JsonSubTypes.Type(value = DeviceTypeFilter.class, name = "deviceType"),
@JsonSubTypes.Type(value = EntityViewTypeFilter.class, name = "entityViewType"),

1
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java

@ -19,6 +19,7 @@ public enum EntityFilterType {
SINGLE_ENTITY("singleEntity"),
ENTITY_LIST("entityList"),
ENTITY_NAME("entityName"),
ENTITY_TYPE("entityType"),
ASSET_TYPE("assetType"),
DEVICE_TYPE("deviceType"),
ENTITY_VIEW_TYPE("entityViewType"),

30
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.query;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
@Data
public class EntityTypeFilter implements EntityFilter {
@Override
public EntityFilterType getType() {
return EntityFilterType.ENTITY_TYPE;
}
private EntityType entityType;
}

5
common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java

@ -18,8 +18,7 @@ package org.thingsboard.server.common.data.query;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.List;
@ -33,7 +32,7 @@ public class RelationsQueryFilter implements EntityFilter {
private EntityId rootEntity;
private EntitySearchDirection direction;
private List<EntityTypeFilter> filters;
private List<RelationEntityTypeFilter> filters;
private int maxLevel;
private boolean fetchLastLevelOnly;

2
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java

@ -26,6 +26,6 @@ import java.util.List;
public class EntityRelationsQuery {
private RelationsSearchParameters parameters;
private List<EntityTypeFilter> filters;
private List<RelationEntityTypeFilter> filters;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityTypeFilter.java → common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java

@ -26,7 +26,7 @@ import java.util.List;
*/
@Data
@AllArgsConstructor
public class EntityTypeFilter {
public class RelationEntityTypeFilter {
private String relationType;

10
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.entity.EntityService;
@ -457,7 +457,7 @@ public class BaseRelationService implements RelationService {
//boolean fetchLastLevelOnly = true;
log.trace("Executing findByQuery [{}]", query);
RelationsSearchParameters params = query.getParameters();
final List<EntityTypeFilter> filters = query.getFilters();
final List<RelationEntityTypeFilter> filters = query.getFilters();
if (filters == null || filters.isEmpty()) {
log.debug("Filters are not set [{}]", query);
}
@ -575,8 +575,8 @@ public class BaseRelationService implements RelationService {
};
}
private boolean matchFilters(List<EntityTypeFilter> filters, EntityRelation relation, EntitySearchDirection direction) {
for (EntityTypeFilter filter : filters) {
private boolean matchFilters(List<RelationEntityTypeFilter> filters, EntityRelation relation, EntitySearchDirection direction) {
for (RelationEntityTypeFilter filter : filters) {
if (match(filter, relation, direction)) {
return true;
}
@ -584,7 +584,7 @@ public class BaseRelationService implements RelationService {
return false;
}
private boolean match(EntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) {
private boolean match(RelationEntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) {
if (StringUtils.isEmpty(filter.getRelationType()) || filter.getRelationType().equals(relation.getType())) {
if (filter.getEntityTypes() == null || filter.getEntityTypes().isEmpty()) {
return true;

84
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -40,12 +40,13 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityNameFilter;
import org.thingsboard.server.common.data.query.EntitySearchQueryFilter;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter;
import org.thingsboard.server.common.data.query.EntityViewTypeFilter;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.Arrays;
import java.util.Collections;
@ -249,18 +250,70 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) {
EntityType entityType = resolveEntityType(query.getEntityFilter());
QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType));
ctx.append("select count(e.id) from ");
ctx.append(addEntityTableQuery(ctx, query.getEntityFilter()));
ctx.append(" e where ");
ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList()));
return transactionTemplate.execute(status -> {
long startTs = System.currentTimeMillis();
try {
return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class);
} finally {
queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs);
if (query.getKeyFilters() == null || query.getKeyFilters().isEmpty()) {
ctx.append("select count(e.id) from ");
ctx.append(addEntityTableQuery(ctx, query.getEntityFilter()));
ctx.append(" e where ");
ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList()));
return transactionTemplate.execute(status -> {
long startTs = System.currentTimeMillis();
try {
return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class);
} finally {
queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs);
}
});
} else {
List<EntityKeyMapping> mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query);
List<EntityKeyMapping> selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection)
.collect(Collectors.toList());
List<EntityKeyMapping> entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest())
.collect(Collectors.toList());
List<EntityKeyMapping> filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter)
.collect(Collectors.toList());
List<EntityKeyMapping> entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest())
.collect(Collectors.toList());
List<EntityKeyMapping> allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest)
.collect(Collectors.toList());
String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping);
String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true);
String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType);
String entityTypeStr;
if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) {
entityTypeStr = "e.entity_type";
} else {
entityTypeStr = "'" + entityType.name() + "'";
}
});
if (!StringUtils.isEmpty(entityFieldsSelection)) {
entityFieldsSelection = String.format("e.id id, %s entity_type, %s", entityTypeStr, entityFieldsSelection);
} else {
entityFieldsSelection = String.format("e.id id, %s entity_type", entityTypeStr);
}
String fromClauseCount = String.format("from (select %s from (select %s from %s e where %s) entities %s ) result %s",
"entities.*",
entityFieldsSelection,
addEntityTableQuery(ctx, query.getEntityFilter()),
entityWhereClause,
latestJoinsCnt,
"");
String countQuery = String.format("select count(id) %s", fromClauseCount);
return transactionTemplate.execute(status -> {
long startTs = System.currentTimeMillis();
try {
return jdbcTemplate.queryForObject(countQuery, ctx, Long.class);
} finally {
queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs);
}
});
}
}
@Override
@ -436,6 +489,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
case ASSET_SEARCH_QUERY:
case ENTITY_VIEW_SEARCH_QUERY:
case API_USAGE_STATE:
case ENTITY_TYPE:
return "";
default:
throw new RuntimeException("Not implemented!");
@ -521,7 +575,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
boolean single = entityFilter.getFilters() != null && entityFilter.getFilters().size() == 1;
if (entityFilter.getFilters() != null && !entityFilter.getFilters().isEmpty()) {
int entityTypeFilterIdx = 0;
for (EntityTypeFilter etf : entityFilter.getFilters()) {
for (RelationEntityTypeFilter etf : entityFilter.getFilters()) {
String etfCondition = buildEtfCondition(ctx, etf, entityFilter.getDirection(), entityTypeFilterIdx++);
if (!etfCondition.isEmpty()) {
if (noConditions) {
@ -570,7 +624,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
return "( " + selectFields + from + ")";
}
private String buildEtfCondition(QueryContext ctx, EntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) {
private String buildEtfCondition(QueryContext ctx, RelationEntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) {
StringBuilder whereFilter = new StringBuilder();
String relationType = etf.getRelationType();
List<EntityType> entityTypes = etf.getEntityTypes();
@ -676,6 +730,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
return ((EntityListFilter) entityFilter).getEntityType();
case ENTITY_NAME:
return ((EntityNameFilter) entityFilter).getEntityType();
case ENTITY_TYPE:
return ((EntityTypeFilter) entityFilter).getEntityType();
case ASSET_TYPE:
case ASSET_SEARCH_QUERY:
return EntityType.ASSET;

25
dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityFilter;
@ -380,6 +381,30 @@ public class EntityKeyMapping {
return mappings;
}
public static List<EntityKeyMapping> prepareEntityCountKeyMapping(EntityCountQuery query) {
Map<EntityKey, List<KeyFilter>> filters =
query.getKeyFilters() != null ?
query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap();
int index = 2;
List<EntityKeyMapping> mappings = new ArrayList<>();
if (!filters.isEmpty()) {
for (EntityKey filterField : filters.keySet()) {
EntityKeyMapping mapping = new EntityKeyMapping();
mapping.setIndex(index);
mapping.setAlias(String.format("alias%s", index));
mapping.setKeyFilters(filters.get(filterField));
mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD));
mapping.setSelection(false);
mapping.setEntityKey(filterField);
mappings.add(mapping);
index += 1;
}
}
return mappings;
}
private String buildAttributeSelection() {
return buildTimeSeriesOrAttrSelection(true);
}

54
dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java

@ -15,9 +15,6 @@
*/
package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
@ -31,25 +28,48 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.*;
import org.thingsboard.server.common.data.query.AssetSearchQueryFilter;
import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.util.DaoTestUtil;
import org.thingsboard.server.dao.util.SqlDbType;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@ -140,13 +160,13 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(30, count);
filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(25, count);
filter.setRootEntity(devices.get(0).getId());
filter.setDirection(EntitySearchDirection.TO);
filter.setFilters(Collections.singletonList(new EntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT))));
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT))));
count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(1, count);
@ -208,7 +228,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId);
filter.setDirection(EntitySearchDirection.FROM);
filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC

6
dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java

@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -221,7 +221,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false));
query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(3, relations.size());
Assert.assertTrue(relations.contains(relationA));
@ -255,7 +255,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false));
query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(2, relations.size());
Assert.assertTrue(relations.contains(relationAB));

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java

@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.data;
import lombok.Data;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.List;
@ -26,6 +26,6 @@ public class RelationsQuery {
private EntitySearchDirection direction;
private int maxLevel = 1;
private List<EntityTypeFilter> filters;
private List<RelationEntityTypeFilter> filters;
private boolean fetchLastLevelOnly = false;
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java

@ -19,7 +19,7 @@ import lombok.Data;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.Collections;
import java.util.HashMap;
@ -41,8 +41,8 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig
RelationsQuery relationsQuery = new RelationsQuery();
relationsQuery.setDirection(EntitySearchDirection.FROM);
relationsQuery.setMaxLevel(1);
EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter));
configuration.setRelationsQuery(relationsQuery);
return configuration;

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java

@ -20,7 +20,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.Collections;
@ -39,8 +39,8 @@ public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfigur
RelationsQuery relationsQuery = new RelationsQuery();
relationsQuery.setDirection(EntitySearchDirection.FROM);
relationsQuery.setMaxLevel(1);
EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter));
configuration.setRelationsQuery(relationsQuery);
return configuration;

1
ui-ngx/package.json

@ -68,6 +68,7 @@
"ngx-clipboard": "^13.0.1",
"ngx-color-picker": "^10.1.0",
"ngx-daterangepicker-material": "^4.0.1",
"ngx-drag-drop": "^2.0.0",
"ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
"ngx-hm-carousel": "^2.0.0-rc.1",
"ngx-sharebuttons": "^8.0.1",

21
ui-ngx/src/app/core/api/alias-controller.ts

@ -16,7 +16,7 @@
import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models';
import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { Datasource, DatasourceType } from '@app/shared/models/widget.models';
import { Datasource, DatasourceType, datasourceTypeTranslationMap } from '@app/shared/models/widget.models';
import { deepClone, isEqual } from '@core/utils';
import { EntityService } from '@core/http/entity.service';
import { UtilsService } from '@core/services/utils.service';
@ -27,6 +27,7 @@ import {
defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
export class AliasController implements IAliasController {
@ -50,6 +51,7 @@ export class AliasController implements IAliasController {
constructor(private utils: UtilsService,
private entityService: EntityService,
private translate: TranslateService,
private stateControllerHolder: StateControllerHolder,
private origEntityAliases: EntityAliases,
private origFilters: Filters) {
@ -241,7 +243,7 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.entity) {
if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount) {
if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
}
@ -329,16 +331,23 @@ export class AliasController implements IAliasController {
return forkJoin(observables).pipe(
map((result) => {
let functionIndex = 0;
let entityCountIndex = 0;
result.forEach((datasource) => {
if (datasource.type === DatasourceType.function) {
if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount) {
let name: string;
if (datasource.name && datasource.name.length) {
name = datasource.name;
} else {
functionIndex++;
name = DatasourceType.function;
if (functionIndex > 1) {
if (datasource.type === DatasourceType.function) {
functionIndex++;
} else {
entityCountIndex++;
}
name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type));
if (datasource.type === DatasourceType.function && functionIndex > 1) {
name += ' ' + functionIndex;
} else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) {
name += ' ' + entityCountIndex;
}
}
datasource.name = name;

95
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -22,12 +22,14 @@ import {
EntityFilter,
EntityKey,
EntityKeyType,
entityKeyTypeToDataKeyType, entityPageDataChanged,
entityKeyTypeToDataKeyType,
entityPageDataChanged,
KeyFilter,
TsValue
} from '@shared/models/query/query.models';
import {
DataKeyType,
EntityCountCmd,
EntityDataCmd,
SubscriptionData,
SubscriptionDataHolder,
@ -42,6 +44,7 @@ import { DataAggregator } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import Timeout = NodeJS.Timeout;
declare type DataKeyFunction = (time: number, prevValue: any) => any;
@ -82,6 +85,7 @@ export class EntityDataSubscription {
private subscriber: TelemetrySubscriber;
private dataCommand: EntityDataCmd;
private subsCommand: EntityDataCmd;
private countCommand: EntityCountCmd;
private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>;
@ -91,7 +95,7 @@ export class EntityDataSubscription {
private pageData: PageData<EntityData>;
private subsTw: SubscriptionTimewindow;
private dataAggregators: Array<DataAggregator>;
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {}
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}};
private entityIdToDataIndex: {[id: string]: number};
@ -125,7 +129,8 @@ export class EntityDataSubscription {
}
}
let key: string;
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`;
} else {
@ -150,7 +155,7 @@ export class EntityDataSubscription {
clearTimeout(this.timer);
this.timer = null;
}
if (this.datasourceType === DatasourceType.entity) {
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount) {
if (this.subscriber) {
this.subscriber.unsubscribe();
this.subscriber = null;
@ -159,7 +164,7 @@ export class EntityDataSubscription {
if (this.dataAggregators) {
this.dataAggregators.forEach((aggregator) => {
aggregator.destroy();
})
});
this.dataAggregators = null;
}
this.pageData = null;
@ -291,6 +296,79 @@ export class EntityDataSubscription {
totalPages: 1
};
this.onPageData(pageData);
} else if (this.datasourceType === DatasourceType.entityCount) {
this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.countCommand = new EntityCountCmd();
let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
if (keyFilters) {
keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters);
} else {
keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters;
}
}
this.countCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter,
keyFilters
};
this.subscriber.subscriptionCommands.push(this.countCommand);
const entityId: EntityId = {
id: NULL_UUID,
entityType: null
};
const countKey = this.entityDataSubscriptionOptions.dataKeys[0];
let dataReceived = false;
this.subscriber.entityCount$.subscribe(
(entityCountUpdate) => {
if (!dataReceived) {
const entityData: EntityData = {
entityId,
latest: {
[EntityKeyType.ENTITY_FIELD]: {
name: {
ts: Date.now(),
value: DatasourceType.entityCount
}
},
[EntityKeyType.COUNT]: {
[countKey.name]: {
ts: Date.now(),
value: entityCountUpdate.count + ''
}
}
},
timeseries: {}
};
const pageData: PageData<EntityData> = {
data: [entityData],
hasNext: false,
totalElements: 1,
totalPages: 1
};
this.onPageData(pageData);
dataReceived = true;
} else {
const update: EntityData[] = [{
entityId,
latest: {
[EntityKeyType.COUNT]: {
[countKey.name]: {
ts: Date.now(),
value: entityCountUpdate.count + ''
}
}
},
timeseries: {}
}];
this.onDataUpdate(update);
}
}
);
this.subscriber.subscribe();
}
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return of(null);
@ -347,7 +425,7 @@ export class EntityDataSubscription {
limit: this.subsTw.aggregation.limit,
agg: this.subsTw.aggregation.type,
fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
}
};
}
}
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
@ -381,7 +459,7 @@ export class EntityDataSubscription {
if (this.dataAggregators) {
this.dataAggregators.forEach((aggregator) => {
aggregator.destroy();
})
});
}
this.dataAggregators = [];
this.resetData();
@ -419,7 +497,8 @@ export class EntityDataSubscription {
this.datasourceData[dataIndex] = {};
for (const key of Object.keys(this.dataKeys)) {
const dataKey = this.dataKeys[key];
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const dataKeysList = dataKey as Array<SubscriptionDataKey>;
for (let index = 0; index < dataKeysList.length; index++) {
this.datasourceData[dataIndex][key + '_' + index] = {

7
ui-ngx/src/app/core/api/entity-data.service.ts

@ -135,11 +135,14 @@ export class EntityDataService {
dataKeys: subscriptionDataKeys,
type: subscriptionType
};
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity ||
entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
entityDataSubscriptionOptions.pageLink = pageLink;
entityDataSubscriptionOptions.keyFilters = keyFilters;
entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
entityDataSubscriptionOptions.pageLink = pageLink;
}
}
entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription;
return entityDataSubscriptionOptions;

15
ui-ngx/src/app/core/http/entity.service.ts

@ -481,6 +481,8 @@ export class EntityService {
return entityTypes.indexOf(filter.entityType) > -1 ? true : false;
case AliasFilterType.entityName:
return entityTypes.indexOf(filter.entityType) > -1 ? true : false;
case AliasFilterType.entityType:
return entityTypes.indexOf(filter.entityType) > -1 ? true : false;
case AliasFilterType.stateEntity:
return true;
case AliasFilterType.assetType:
@ -540,6 +542,8 @@ export class EntityService {
return true;
case AliasFilterType.entityName:
return true;
case AliasFilterType.entityType:
return true;
case AliasFilterType.stateEntity:
return true;
case AliasFilterType.assetType:
@ -805,6 +809,9 @@ export class EntityService {
case AliasFilterType.entityName:
result.entityFilter = deepClone(filter);
return of(result);
case AliasFilterType.entityType:
result.entityFilter = deepClone(filter);
return of(result);
case AliasFilterType.stateEntity:
result.stateEntity = true;
if (stateEntityId) {
@ -1099,10 +1106,10 @@ export class EntityService {
dataKeys: []
};
this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo);
} else if (subscriptionInfo.type === DatasourceType.function) {
} else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount) {
datasource = {
type: subscriptionInfo.type,
name: subscriptionInfo.name || DatasourceType.function,
name: subscriptionInfo.name || subscriptionInfo.type,
dataKeys: []
};
}
@ -1119,6 +1126,10 @@ export class EntityService {
if (subscriptionInfo.alarmFields) {
this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource);
}
if (subscriptionInfo.type === DatasourceType.entityCount) {
const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count);
datasource.dataKeys.push(dataKey);
}
}
return datasource;
}

4
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -93,7 +93,7 @@ export class ItemBufferService {
if (widget.config.datasources) {
for (let i = 0; i < widget.config.datasources.length; i++) {
const datasource = widget.config.datasources[i];
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) {
if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.entityAliasId) {
entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
if (entityAlias) {
aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias);
@ -119,7 +119,7 @@ export class ItemBufferService {
if (widget.config.datasources) {
for (let i = 0; i < widget.config.datasources.length; i++) {
const datasource = widget.config.datasources[i];
if (datasource.type === DatasourceType.entity && datasource.filterId) {
if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.filterId) {
filter = dashboard.configuration.filters[datasource.filterId];
if (filter) {
filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter);

3
ui-ngx/src/app/core/services/utils.service.ts

@ -27,7 +27,8 @@ import {
deleteNullProperties,
guid,
isDefined,
isDefinedAndNotNull, isString,
isDefinedAndNotNull,
isString,
isUndefined
} from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';

32
ui-ngx/src/app/core/ws/telemetry-websocket.service.ts

@ -16,18 +16,27 @@
import { Inject, Injectable, NgZone } from '@angular/core';
import {
AlarmDataCmd, AlarmDataUnsubscribeCmd,
AlarmDataCmd,
AlarmDataUnsubscribeCmd,
AlarmDataUpdate,
AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate,
GetHistoryCmd, isAlarmDataUpdateMsg, isEntityDataUpdateMsg,
AttributesSubscriptionCmd,
EntityCountCmd, EntityCountUnsubscribeCmd,
EntityCountUpdate,
EntityDataCmd,
EntityDataUnsubscribeCmd,
EntityDataUpdate,
GetHistoryCmd,
isAlarmDataUpdateMsg,
isEntityCountUpdateMsg,
isEntityDataUpdateMsg,
SubscriptionCmd,
SubscriptionUpdate,
SubscriptionUpdateMsg,
TelemetryFeature,
TelemetryPluginCmdsWrapper,
TelemetryService,
TelemetrySubscriber,
TimeseriesSubscriptionCmd, WebsocketDataMsg
TimeseriesSubscriptionCmd,
WebsocketDataMsg
} from '@app/shared/models/telemetry/telemetry.models';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -111,6 +120,8 @@ export class TelemetryWebsocketService implements TelemetryService {
this.cmdsWrapper.entityDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof AlarmDataCmd) {
this.cmdsWrapper.alarmDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof EntityCountCmd) {
this.cmdsWrapper.entityCountCmds.push(subscriptionCommand);
}
}
);
@ -150,6 +161,10 @@ export class TelemetryWebsocketService implements TelemetryService {
const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd();
alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdsWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd);
} else if (subscriptionCommand instanceof EntityCountCmd) {
const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd();
entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdsWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd);
}
const cmdId = subscriptionCommand.cmdId;
if (cmdId) {
@ -236,7 +251,7 @@ export class TelemetryWebsocketService implements TelemetryService {
{
url: uri,
openObserver: {
next: (e: Event) => {
next: () => {
this.onOpen();
}
},
@ -294,6 +309,11 @@ export class TelemetryWebsocketService implements TelemetryService {
if (subscriber) {
subscriber.onAlarmData(new AlarmDataUpdate(message));
}
} else if (isEntityCountUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onEntityCount(new EntityCountUpdate(message));
}
} else if (message.subscriptionId) {
subscriber = this.subscribersMap.get(message.subscriptionId);
if (subscriber) {

1
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -403,6 +403,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
this.aliasController = new AliasController(this.utils,
this.entityService,
this.translate,
() => stateController, entitiAliases, filters);
const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ?

1
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -312,6 +312,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardCtx.aliasController = new AliasController(this.utils,
this.entityService,
this.translate,
() => this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases,
this.dashboardConfiguration.filters);

4
ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts

@ -76,6 +76,10 @@ export class EntityFilterViewComponent implements ControlValueAccessor {
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith,
{prefix});
break;
case AliasFilterType.entityType:
entityType = this.filter.entityType;
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).typePlural);
break;
case AliasFilterType.stateEntity:
this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description');
break;

7
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -59,6 +59,13 @@
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityType">
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="allowedEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.stateEntity">
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>alias.state-entity-parameter-name</mat-label>

5
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts

@ -123,6 +123,11 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit {
entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]],
});
break;
case AliasFilterType.entityType:
this.filterFormGroup = this.fb.group({
entityType: [filter ? filter.entityType : null, [Validators.required]]
});
break;
case AliasFilterType.stateEntity:
this.filterFormGroup = this.fb.group({
stateEntityParamName: [filter ? filter.stateEntityParamName : null, []],

21
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts

@ -107,12 +107,15 @@ export class KeyFilterDialogComponent extends
key: [this.data.keyFilter.key.key, [Validators.required]]
}
),
value: [this.data.keyFilter.value],
valueType: [this.data.keyFilter.valueType, [Validators.required]],
predicates: [this.data.keyFilter.predicates, [Validators.required]]
}
);
if (this.data.telemetryKeysOnly) {
this.keyFilterFormGroup.addControl(
'value', this.fb.control(this.data.keyFilter.value)
);
}
if (!this.data.readonly) {
this.keyFilterFormGroup.get('valueType').valueChanges.pipe(
takeUntil(this.destroy$)
@ -144,12 +147,14 @@ export class KeyFilterDialogComponent extends
} else {
this.showAutocomplete = false;
}
if (type === EntityKeyType.CONSTANT) {
this.keyFilterFormGroup.get('value').setValidators(Validators.required);
this.keyFilterFormGroup.get('value').updateValueAndValidity();
} else {
this.keyFilterFormGroup.get('value').clearValidators();
this.keyFilterFormGroup.get('value').updateValueAndValidity();
if (this.data.telemetryKeysOnly) {
if (type === EntityKeyType.CONSTANT) {
this.keyFilterFormGroup.get('value').setValidators(Validators.required);
this.keyFilterFormGroup.get('value').updateValueAndValidity();
} else {
this.keyFilterFormGroup.get('value').clearValidators();
this.keyFilterFormGroup.get('value').updateValueAndValidity();
}
}
});

10
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts

@ -24,7 +24,7 @@ import {
NG_VALUE_ACCESSOR
} from '@angular/forms';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { EntityTypeFilter } from '@shared/models/relation.models';
import { RelationEntityTypeFilter } from '@shared/models/relation.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -80,7 +80,7 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
this.disabled = isDisabled;
}
writeValue(filters: Array<EntityTypeFilter>): void {
writeValue(filters: Array<RelationEntityTypeFilter>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
@ -102,14 +102,14 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
public addFilter() {
const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray;
const filter: EntityTypeFilter = {
const filter: RelationEntityTypeFilter = {
relationType: null,
entityTypes: []
};
relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
}
private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl {
private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl {
return this.fb.group({
relationType: [filter ? filter.relationType : null],
entityTypes: [filter ? filter.entityTypes : []]
@ -117,7 +117,7 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
}
private updateModel() {
const filters: Array<EntityTypeFilter> = this.relationFiltersFormGroup.get('relationFilters').value;
const filters: Array<RelationEntityTypeFilter> = this.relationFiltersFormGroup.get('relationFilters').value;
this.propagateChange(filters);
}
}

4
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html

@ -25,7 +25,7 @@
formControlName="name"
required
[matAutocomplete]="keyAutocomplete">
<button *ngIf="dataKeyFormGroup.get('name').value"
<button *ngIf="dataKeyFormGroup.get('name').value && !dataKeyFormGroup.get('name').disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearKey()">
@ -72,7 +72,7 @@
formControlName="funcBody">
</tb-js-func>
</section>
<section fxLayout="column" *ngIf="(modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute) && showPostProcessing">
<section fxLayout="column" *ngIf="(modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute || modelValue.type === dataKeyTypes.count) && showPostProcessing">
<mat-checkbox formControlName="usePostProcessing">
{{ 'datakey.use-data-post-processing-func' | translate }}
</mat-checkbox>

9
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts

@ -176,7 +176,14 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
this.modelValue.usePostProcessing = true;
}
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []);
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function &&
this.modelValue.type !== DataKeyType.count
? [Validators.required] : []);
if (this.modelValue.type === DataKeyType.count) {
this.dataKeyFormGroup.get('name').disable({emitEvent: false});
} else {
this.dataKeyFormGroup.get('name').enable({emitEvent: false});
}
this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false});
if (this.displayAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings;

10
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -20,7 +20,7 @@
<mat-chip
*ngFor="let key of keys; let $index = index"
[selectable]="!disabled"
[removable]="!disabled"
[removable]="!disabled && datasourceType !== datasourceTypes.entityCount"
(removed)="remove(key)">
<div fxLayout="row" fxLayoutAlign="start center" class="tb-attribute-chip">
<div *ngIf="!disabled" class="tb-chip-drag-handle" style="margin-right: 5px;">
@ -31,7 +31,7 @@
</div>
<div class="tb-chip-labels">
<div class="tb-chip-label">
<span *ngIf="datasourceType !== datasourceTypes.function">
<span *ngIf="datasourceType !== datasourceTypes.function && datasourceType !== datasourceTypes.entityCount">
<span *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">
@ -70,7 +70,7 @@
(click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32">
<mat-icon class="tb-mat-20">edit</mat-icon>
</button>
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
<mat-icon matChipRemove *ngIf="!disabled && datasourceType !== datasourceTypes.entityCount">close</mat-icon>
</div>
</mat-chip>
<input matInput type="text" placeholder="{{ !disabled ? placeholder : '' }}"
@ -91,7 +91,7 @@
[displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;">
<span *ngIf="datasourceType !== datasourceTypes.function">
<span *ngIf="datasourceType !== datasourceTypes.function && datasourceType !== datasourceTypes.entityCount">
<span *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">
@ -128,7 +128,7 @@
{{ translate.get('entity.no-key-matching',
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span *ngIf="datasourceType === datasourceTypes.function; else createEntityKey">
<span *ngIf="datasourceType === datasourceTypes.function || datasourceType === datasourceTypes.entityCount; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a>
</span>
<ng-template #createEntityKey>

59
ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts

@ -16,26 +16,26 @@
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import {
AfterViewInit,
Component,
ElementRef,
forwardRef,
Input,
OnChanges,
OnInit,
SimpleChanges,
SkipSelf,
ViewChild
AfterViewInit,
Component,
ElementRef,
forwardRef,
Input,
OnChanges,
OnInit,
SimpleChanges,
SkipSelf,
ViewChild
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR,
NgForm,
Validators
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR,
NgForm,
Validators
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
@ -56,8 +56,8 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/data-key-config-dialog.component';
import { deepClone } from '@core/utils';
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive';
@ -94,8 +94,15 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
@Input()
datasourceType: DatasourceType;
private maxDataKeysValue: number;
get maxDataKeys(): number {
return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue;
}
@Input()
maxDataKeys: number;
set maxDataKeys(value: number) {
this.maxDataKeysValue = value;
}
@Input()
optDataKeys: boolean;
@ -114,7 +121,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue || !this.optDataKeys;
return this.requiredValue || !this.optDataKeys || this.datasourceType === DatasourceType.entityCount;
}
@Input()
set required(value: boolean) {
@ -210,8 +217,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
if (this.maxDataKeys !== null && this.maxDataKeys > -1) {
if (this.datasourceType === DatasourceType.function) {
return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys});
} else {
} else if (this.datasourceType !== DatasourceType.entityCount) {
return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys});
} else {
return '';
}
} else {
return '';
@ -241,6 +250,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private reset() {
if (this.widgetType === widgetType.alarm) {
this.keys = this.utils.getDefaultAlarmDataKeys();
} else if (this.datasourceType === DatasourceType.entityCount) {
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count)];
} else {
this.keys = [];
}
@ -426,7 +437,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList);
} else {
if (this.entityAliasId) {
if (this.datasourceType !== DatasourceType.entityCount && this.entityAliasId) {
const dataKeyTypes = [DataKeyType.timeseries];
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute);

14
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts

@ -84,7 +84,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private datasources: Array<HierarchyNodeDatasource>;
private data: Array<Array<DatasourceData>>;
private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {};
private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {};
@ -121,7 +120,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.datasources = this.subscription.datasources as Array<HierarchyNodeDatasource>;
this.data = this.subscription.dataPages[0].data;
this.initializeConfig();
this.ctx.updateWidgetParams();
}
@ -252,12 +250,16 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
public loadNodes: LoadNodesCallback = (node, cb) => {
if (node.id === '#') {
const childNodes: HierarchyNavTreeNode[] = [];
let dataIndex = 0;
this.datasources.forEach((childDatasource, index) => {
childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, this.data[index]));
const datasourceData = this.subscription.data.slice(dataIndex);
childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, datasourceData));
dataIndex += childDatasource.dataKeys.length;
});
cb(this.prepareNodes(childNodes));
} else {
if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') {
if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.datasource.type === DatasourceType.entity
&& node.data.nodeCtx.entity.id.entityType !== 'function') {
this.loadChildren(node, node.data.datasource, cb);
/* (error) => { // TODO:
let errorText = 'Failed to get relations!';
@ -369,8 +371,8 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
}
private datasourceToNode(datasource: HierarchyNodeDatasource,
data: DatasourceData[],
parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode {
data: DatasourceData[],
parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode {
const node: HierarchyNavTreeNode = {
id: (++this.nodeIdCounter) + ''
};

11
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts

@ -153,7 +153,16 @@ export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => {
};
export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => {
let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);
let result = 0;
if (!nodeCtx1.entity.id.entityType || !nodeCtx2.entity.id.entityType ) {
if (nodeCtx1.entity.id.entityType) {
result = 1;
} else if (nodeCtx2.entity.id.entityType) {
result = -1;
}
} else {
result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);
}
if (result === 0) {
result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);
}

170
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -100,87 +100,115 @@
</div>
<ng-template #datasourcesTemplate>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<span fxFlex="5"></span>
<span style="width: 60px;"></span>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate style="min-width: 110px;">widget-config.datasource-type</span>
<span translate style="min-width: 120px;">widget-config.datasource-type</span>
<span fxHide fxShow.gt-sm translate fxFlex
style="padding-left: 10px;">widget-config.datasource-parameters</span>
<span style="min-width: 40px;"></span>
</div>
</div>
<div style="overflow: auto; padding-bottom: 15px;">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
formArrayName="datasources"
*ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;">
<span fxFlex="5">{{$index + 1}}.</span>
<div [formGroupName]="$index" class="mat-elevation-z4" fxFlex
fxLayout="row"
fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<section fxFlex
fxLayout="column"
fxLayoutAlign="center"
fxLayout.gt-sm="row"
fxLayoutAlign.gt-sm="start center">
<mat-form-field class="tb-datasource-type">
<mat-select formControlName="type">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<section class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field floatLabel="always"
class="tb-datasource-name" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.name' | translate }}"
formControlName="name">
<mat-list dndDropzone dndEffectAllowed="move"
(dndDrop)="onDatasourceDrop($event)"
[dndDisableIf]="disabled" formArrayName="datasources">
<mat-list-item dndPlaceholderRef
class="dndPlaceholder">
</mat-list-item>
<mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"
[dndDraggable]="datasourceControl.value"
(dndMoved)="dndDatasourceMoved($index)"
[dndDisableIf]="disabled"
dndEffectAllowed="move">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div style="width: 60px;">
<button *ngIf="!disabled" mat-icon-button color="primary"
class="handle"
style="min-width: 40px; margin: 0"
dndHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_handle</mat-icon>
</button>
<span>{{$index + 1}}.</span>
</div>
<div class="mat-elevation-z4" fxFlex
fxLayout="row"
fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<section fxFlex
fxLayout="column"
fxLayoutAlign="center"
fxLayout.gt-sm="row"
fxLayoutAlign.gt-sm="start center">
<mat-form-field class="tb-datasource-type">
<mat-select [formControl]="datasourceControl.get('type')">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceType.entity">
<section fxLayout="column">
<tb-entity-alias-select
[showLabel]="true"
[tbRequired]="datasourceControl.get('type').value === datasourceType.entity"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
formControlName="filterId"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
<section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field floatLabel="always"
class="tb-datasource-name" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''">
<tb-entity-alias-select
[showLabel]="true"
[tbRequired]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('entityAliasId')"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('filterId')"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
<mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount"
floatLabel="always"
class="tb-datasource-name no-border-top" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
</section>
</ng-template>
</section>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
formControlName="dataKeys">
</tb-data-keys>
</section>
<button [disabled]="isLoading$ | async"
type="button"
mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('dataKeys')">
</tb-data-keys>
</section>
<button [disabled]="isLoading$ | async"
type="button"
mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</mat-list-item>
</mat-list>
</div>
</ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">

40
ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss

@ -23,10 +23,34 @@
.tb-advanced-widget-config {
height: 100%;
}
.tb-datasources {
.handle {
cursor: move;
}
.mat-list {
min-height: 68px;
padding-left: 0;
}
.mat-list-item {
height: auto;
min-height: 68px;
display: block;
&.dndDraggingSource {
display: none;
}
&.dndPlaceholder {
display: block;
background-color: #ddd;
}
}
}
.tb-datasource-type {
min-width: 110px;
min-width: 120px;
@media #{$mat-gt-sm} {
max-width: 110px;
max-width: 120px;
}
}
.tb-datasource {
@ -70,13 +94,18 @@
white-space: normal;
}
.mat-expansion-panel {
&.tb-datasources{
&.tb-datasources {
&.mat-expanded {
overflow: visible;
}
.mat-expansion-panel-body{
padding: 0 12px 16px;
}
.mat-list-base .mat-list-item {
.mat-list-item-content {
padding: 0;
}
}
}
.mat-expansion-panel-content {
font: inherit;
@ -110,6 +139,11 @@
}
}
}
.tb-datasource-name.no-border-top {
.mat-form-field-infix {
border-top: 0;
}
}
}
.tb-data-keys {
@media #{$mat-gt-sm} {

51
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -40,7 +40,7 @@ import {
Validators
} from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined, isObject } from '@app/core/utils';
import { deepClone, isDefined, isObject, isUndefined } from '@app/core/utils';
import {
alarmFields,
AlarmSearchStatus,
@ -71,6 +71,7 @@ import { Filter, Filters } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { DndDropEvent } from 'ngx-drag-drop';
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -141,7 +142,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetType: widgetType;
datasourceType = DatasourceType;
datasourceTypes: Array<DatasourceType>;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap;
widgetConfigCallbacks: WidgetConfigCallbacks = {
@ -186,11 +187,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
ngOnInit(): void {
if (this.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
}
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({});
@ -295,6 +291,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
private buildForms() {
if (this.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
if (this.widgetType === widgetType.latest) {
this.datasourceTypes.push(DatasourceType.entityCount);
}
}
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({});
@ -517,22 +521,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
private buildDatasourceForm(datasource?: Datasource): FormGroup {
const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional;
let dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional
|| datasource?.type === DatasourceType.entityCount;
const datasourceFormGroup = this.fb.group(
{
type: [datasource ? datasource.type : null, [Validators.required]],
name: [datasource ? datasource.name : null, []],
entityAliasId: [datasource ? datasource.entityAliasId : null,
datasource && datasource.type === DatasourceType.entity ? [Validators.required] : []],
datasource && (datasource.type === DatasourceType.entity ||
datasource.type === DatasourceType.entityCount) ? [Validators.required] : []],
filterId: [datasource ? datasource.filterId : null, []],
dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []]
}
);
datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => {
datasourceFormGroup.get('entityAliasId').setValidators(
type === DatasourceType.entity ? [Validators.required] : []
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
);
dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional
|| type === DatasourceType.entityCount;
datasourceFormGroup.get('dataKeys').setValidators(dataKeysRequired ? [Validators.required] : []);
datasourceFormGroup.get('entityAliasId').updateValueAndValidity();
datasourceFormGroup.get('dataKeys').updateValueAndValidity();
});
return datasourceFormGroup;
}
@ -659,8 +669,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema;
}
public dndDatasourceMoved(index: number) {
this.datasourcesFormArray().removeAt(index);
}
public onDatasourceDrop(event: DndDropEvent) {
let index = event.index;
if (isUndefined(index)) {
index = this.datasourcesFormArray().length;
}
this.datasourcesFormArray().insert(index,
this.buildDatasourceForm(event.data)
);
}
public removeDatasource(index: number) {
(this.dataSettings.get('datasources') as FormArray).removeAt(index);
this.datasourcesFormArray().removeAt(index);
}
public addDatasource() {
@ -673,8 +697,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
dataKeys: []
};
}
const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray;
datasourcesFormArray.push(this.buildDatasourceForm(newDatasource));
this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource));
}
public generateDataKey(chip: any, type: DataKeyType): DataKey {
@ -704,6 +727,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
if (!result.funcBody) {
result.funcBody = 'return prevValue + 1;';
}
} else if (type === DataKeyType.count) {
result.name = 'count';
}
if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) {
result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema);

1
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts

@ -84,6 +84,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
aliasController: IAliasController = new AliasController(this.utils,
this.entityService,
this.translate,
() => { return {
getStateParams(): StateParams {
return {};

2
ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts

@ -124,6 +124,7 @@ class DraggableChip {
if (this.preventDrag) {
event.preventDefault();
} else {
event.stopPropagation();
this.dragging = true;
globalDraggingChipListId = this.chipListElement.id;
this.chipListElement.classList.add(draggingClassName);
@ -159,6 +160,7 @@ class DraggableChip {
}
private onDragEnd(event: Event | any) {
event.stopPropagation();
this.dragging = false;
globalDraggingChipListId = null;
this.chipListElement.classList.remove(draggingClassName);

12
ui-ngx/src/app/shared/models/alias.models.ts

@ -16,14 +16,14 @@
import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models';
import { EntityInfo } from './entity.models';
import { EntitySearchDirection, RelationEntityTypeFilter } from '@shared/models/relation.models';
import { EntityFilter } from '@shared/models/query/query.models';
export enum AliasFilterType {
singleEntity = 'singleEntity',
entityList = 'entityList',
entityName = 'entityName',
entityType = 'entityType',
stateEntity = 'stateEntity',
assetType = 'assetType',
deviceType = 'deviceType',
@ -40,6 +40,7 @@ export const aliasFilterTypeTranslationMap = new Map<AliasFilterType, string>(
[ AliasFilterType.singleEntity, 'alias.filter-type-single-entity' ],
[ AliasFilterType.entityList, 'alias.filter-type-entity-list' ],
[ AliasFilterType.entityName, 'alias.filter-type-entity-name' ],
[ AliasFilterType.entityType, 'alias.filter-type-entity-type' ],
[ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ],
[ AliasFilterType.assetType, 'alias.filter-type-asset-type' ],
[ AliasFilterType.deviceType, 'alias.filter-type-device-type' ],
@ -66,6 +67,10 @@ export interface EntityNameFilter {
entityNameFilter?: string;
}
export interface EntityTypeFilter {
entityType?: EntityType;
}
export interface StateEntityFilter {
stateEntityParamName?: string;
defaultStateEntity?: EntityId;
@ -92,7 +97,7 @@ export interface RelationsQueryFilter {
defaultStateEntity?: EntityId;
rootEntity?: EntityId;
direction?: EntitySearchDirection;
filters?: Array<EntityTypeFilter>;
filters?: Array<RelationEntityTypeFilter>;
maxLevel?: number;
fetchLastLevelOnly?: boolean;
}
@ -129,6 +134,7 @@ export type EntityFilters =
SingleEntityFilter &
EntityListFilter &
EntityNameFilter &
EntityTypeFilter &
StateEntityFilter &
AssetTypeFilter &
DeviceTypeFilter &

13
ui-ngx/src/app/shared/models/query/query.models.ts

@ -36,7 +36,8 @@ export enum EntityKeyType {
TIME_SERIES = 'TIME_SERIES',
ENTITY_FIELD = 'ENTITY_FIELD',
ALARM_FIELD = 'ALARM_FIELD',
CONSTANT = 'CONSTANT'
CONSTANT = 'CONSTANT',
COUNT = 'COUNT'
}
export const entityKeyTypeTranslationMap = new Map<EntityKeyType, string>(
@ -61,6 +62,8 @@ export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKe
return DataKeyType.entityField;
case EntityKeyType.ALARM_FIELD:
return DataKeyType.alarm;
case EntityKeyType.COUNT:
return DataKeyType.count;
}
}
@ -76,6 +79,8 @@ export function dataKeyTypeToEntityKeyType(dataKeyType: DataKeyType): EntityKeyT
return EntityKeyType.ALARM_FIELD;
case DataKeyType.entityField:
return EntityKeyType.ENTITY_FIELD;
case DataKeyType.count:
return EntityKeyType.COUNT;
}
}
@ -346,14 +351,14 @@ export interface KeyFilterPredicateInfo {
export interface KeyFilter {
key: EntityKey;
valueType: EntityKeyValueType;
value: string | number | boolean;
value?: string | number | boolean;
predicate: KeyFilterPredicate;
}
export interface KeyFilterInfo {
key: EntityKey;
valueType: EntityKeyValueType;
value: string | number | boolean;
value?: string | number | boolean;
predicates: Array<KeyFilterPredicateInfo>;
}
@ -716,13 +721,13 @@ export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntity
export interface EntityCountQuery {
entityFilter: EntityFilter;
keyFilters?: Array<KeyFilter>;
}
export interface AbstractDataQuery<T extends EntityDataPageLink> extends EntityCountQuery {
pageLink: T;
entityFields?: Array<EntityKey>;
latestValues?: Array<EntityKey>;
keyFilters?: Array<KeyFilter>;
}
export interface EntityDataQuery extends AbstractDataQuery<EntityDataPageLink> {

4
ui-ngx/src/app/shared/models/relation.models.ts

@ -52,7 +52,7 @@ export const directionTypeTranslations = new Map<EntitySearchDirection, string>(
]
);
export interface EntityTypeFilter {
export interface RelationEntityTypeFilter {
relationType: string;
entityTypes: Array<EntityType>;
}
@ -68,7 +68,7 @@ export interface RelationsSearchParameters {
export interface EntityRelationsQuery {
parameters: RelationsSearchParameters;
filters: Array<EntityTypeFilter>;
filters: Array<RelationEntityTypeFilter>;
}
export interface EntitySearchQuery {

111
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -23,7 +23,7 @@ import { map } from 'rxjs/operators';
import { NgZone } from '@angular/core';
import {
AlarmData,
AlarmDataQuery,
AlarmDataQuery, EntityCountQuery,
EntityData,
EntityDataQuery,
EntityKey,
@ -36,7 +36,8 @@ export enum DataKeyType {
attribute = 'attribute',
function = 'function',
alarm = 'alarm',
entityField = 'entityField'
entityField = 'entityField',
count = 'count'
}
export enum LatestTelemetry {
@ -181,6 +182,11 @@ export class EntityDataCmd implements WebsocketCmd {
}
}
export class EntityCountCmd implements WebsocketCmd {
cmdId: number;
query?: EntityCountQuery;
}
export class AlarmDataCmd implements WebsocketCmd {
cmdId: number;
query?: AlarmDataQuery;
@ -194,6 +200,10 @@ export class EntityDataUnsubscribeCmd implements WebsocketCmd {
cmdId: number;
}
export class EntityCountUnsubscribeCmd implements WebsocketCmd {
cmdId: number;
}
export class AlarmDataUnsubscribeCmd implements WebsocketCmd {
cmdId: number;
}
@ -206,6 +216,8 @@ export class TelemetryPluginCmdsWrapper {
entityDataUnsubscribeCmds: Array<EntityDataUnsubscribeCmd>;
alarmDataCmds: Array<AlarmDataCmd>;
alarmDataUnsubscribeCmds: Array<AlarmDataUnsubscribeCmd>;
entityCountCmds: Array<EntityCountCmd>;
entityCountUnsubscribeCmds: Array<EntityCountUnsubscribeCmd>;
constructor() {
this.attrSubCmds = [];
@ -215,6 +227,8 @@ export class TelemetryPluginCmdsWrapper {
this.entityDataUnsubscribeCmds = [];
this.alarmDataCmds = [];
this.alarmDataUnsubscribeCmds = [];
this.entityCountCmds = [];
this.entityCountUnsubscribeCmds = [];
}
public hasCommands(): boolean {
@ -224,7 +238,9 @@ export class TelemetryPluginCmdsWrapper {
this.entityDataCmds.length > 0 ||
this.entityDataUnsubscribeCmds.length > 0 ||
this.alarmDataCmds.length > 0 ||
this.alarmDataUnsubscribeCmds.length > 0;
this.alarmDataUnsubscribeCmds.length > 0 ||
this.entityCountCmds.length > 0 ||
this.entityCountUnsubscribeCmds.length > 0;
}
public clear() {
@ -235,6 +251,8 @@ export class TelemetryPluginCmdsWrapper {
this.entityDataUnsubscribeCmds.length = 0;
this.alarmDataCmds.length = 0;
this.alarmDataUnsubscribeCmds.length = 0;
this.entityCountCmds.length = 0;
this.entityCountUnsubscribeCmds.length = 0;
}
public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper {
@ -253,6 +271,10 @@ export class TelemetryPluginCmdsWrapper {
preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount);
leftCount -= preparedWrapper.alarmDataCmds.length;
preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount);
leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length;
preparedWrapper.entityCountCmds = this.popCmds(this.entityCountCmds, leftCount);
leftCount -= preparedWrapper.entityCountCmds.length;
preparedWrapper.entityCountUnsubscribeCmds = this.popCmds(this.entityCountUnsubscribeCmds, leftCount);
return preparedWrapper;
}
@ -280,40 +302,54 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder {
errorMsg: string;
}
export enum DataUpdateType {
export enum CmdUpdateType {
ENTITY_DATA = 'ENTITY_DATA',
ALARM_DATA = 'ALARM_DATA'
ALARM_DATA = 'ALARM_DATA',
COUNT_DATA = 'COUNT_DATA'
}
export interface DataUpdateMsg<T> {
export interface CmdUpdateMsg {
cmdId: number;
data?: PageData<T>;
update?: Array<T>;
errorCode: number;
errorMsg: string;
dataUpdateType: DataUpdateType;
cmdUpdateType: CmdUpdateType;
}
export interface DataUpdateMsg<T> extends CmdUpdateMsg {
data?: PageData<T>;
update?: Array<T>;
}
export interface EntityDataUpdateMsg extends DataUpdateMsg<EntityData> {
dataUpdateType: DataUpdateType.ENTITY_DATA;
cmdUpdateType: CmdUpdateType.ENTITY_DATA;
}
export interface AlarmDataUpdateMsg extends DataUpdateMsg<AlarmData> {
dataUpdateType: DataUpdateType.ALARM_DATA;
cmdUpdateType: CmdUpdateType.ALARM_DATA;
allowedEntities: number;
totalEntities: number;
}
export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | SubscriptionUpdateMsg;
export interface EntityCountUpdateMsg extends CmdUpdateMsg {
cmdUpdateType: CmdUpdateType.COUNT_DATA;
count: number;
}
export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg;
export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg {
const updateMsg = (message as DataUpdateMsg<any>);
return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ENTITY_DATA;
const updateMsg = (message as CmdUpdateMsg);
return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ENTITY_DATA;
}
export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg {
const updateMsg = (message as DataUpdateMsg<any>);
return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ALARM_DATA;
const updateMsg = (message as CmdUpdateMsg);
return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ALARM_DATA;
}
export function isEntityCountUpdateMsg(message: WebsocketDataMsg): message is EntityCountUpdateMsg {
const updateMsg = (message as CmdUpdateMsg);
return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.COUNT_DATA;
}
export class SubscriptionUpdate implements SubscriptionUpdateMsg {
@ -365,21 +401,28 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg {
}
}
export class DataUpdate<T> implements DataUpdateMsg<T> {
export class CmdUpdate implements CmdUpdateMsg {
cmdId: number;
errorCode: number;
errorMsg: string;
data?: PageData<T>;
update?: Array<T>;
dataUpdateType: DataUpdateType;
cmdUpdateType: CmdUpdateType;
constructor(msg: DataUpdateMsg<T>) {
constructor(msg: CmdUpdateMsg) {
this.cmdId = msg.cmdId;
this.errorCode = msg.errorCode;
this.errorMsg = msg.errorMsg;
this.cmdUpdateType = msg.cmdUpdateType;
}
}
export class DataUpdate<T> extends CmdUpdate implements DataUpdateMsg<T> {
data?: PageData<T>;
update?: Array<T>;
constructor(msg: DataUpdateMsg<T>) {
super(msg);
this.data = msg.data;
this.update = msg.update;
this.dataUpdateType = msg.dataUpdateType;
}
}
@ -400,6 +443,15 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
}
}
export class EntityCountUpdate extends CmdUpdate {
count: number;
constructor(msg: EntityCountUpdateMsg) {
super(msg);
this.count = msg.count;
}
}
export interface TelemetryService {
subscribe(subscriber: TelemetrySubscriber);
update(subscriber: TelemetrySubscriber);
@ -411,6 +463,7 @@ export class TelemetrySubscriber {
private dataSubject = new ReplaySubject<SubscriptionUpdate>(1);
private entityDataSubject = new ReplaySubject<EntityDataUpdate>(1);
private alarmDataSubject = new ReplaySubject<AlarmDataUpdate>(1);
private entityCountSubject = new ReplaySubject<EntityCountUpdate>(1);
private reconnectSubject = new Subject();
private zone: NgZone;
@ -420,6 +473,7 @@ export class TelemetrySubscriber {
public data$ = this.dataSubject.asObservable();
public entityData$ = this.entityDataSubject.asObservable();
public alarmData$ = this.alarmDataSubject.asObservable();
public entityCount$ = this.entityCountSubject.asObservable();
public reconnect$ = this.reconnectSubject.asObservable();
public static createEntityAttributesSubscription(telemetryService: TelemetryService,
@ -464,6 +518,7 @@ export class TelemetrySubscriber {
this.dataSubject.complete();
this.entityDataSubject.complete();
this.alarmDataSubject.complete();
this.entityCountSubject.complete();
this.reconnectSubject.complete();
}
@ -513,6 +568,18 @@ export class TelemetrySubscriber {
}
}
public onEntityCount(message: EntityCountUpdate) {
if (this.zone) {
this.zone.run(
() => {
this.entityCountSubject.next(message);
}
);
} else {
this.entityCountSubject.next(message);
}
}
public onReconnected() {
this.reconnectSubject.next();
}

6
ui-ngx/src/app/shared/models/widget.models.ts

@ -246,13 +246,15 @@ export interface DataKey extends KeyInfo {
export enum DatasourceType {
function = 'function',
entity = 'entity'
entity = 'entity',
entityCount = 'entityCount'
}
export const datasourceTypeTranslationMap = new Map<DatasourceType, string>(
[
[ DatasourceType.function, 'function.function' ],
[ DatasourceType.entity, 'entity.entity' ]
[ DatasourceType.entity, 'entity.entity' ],
[ DatasourceType.entityCount, 'entity.entities-count' ]
]
);

6
ui-ngx/src/app/shared/shared.module.ts

@ -52,6 +52,7 @@ import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatListModule } from '@angular/material/list';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material';
import { GridsterModule } from 'angular-gridster2';
@ -132,6 +133,7 @@ import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-t
import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component';
import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component';
import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component';
import { DndModule } from 'ngx-drag-drop';
import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
import { ContactComponent } from '@shared/components/contact.component';
import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component';
@ -259,6 +261,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
MatStepperModule,
MatAutocompleteModule,
MatChipsModule,
MatListModule,
GridsterModule,
ClipboardModule,
FlexLayoutModule.withConfig({addFlexToParent: false}),
@ -269,6 +272,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HotkeyModule,
ColorPickerModule,
NgxHmCarouselModule,
DndModule,
NgxFlowModule,
NgxFlowchartModule
],
@ -348,6 +352,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
MatStepperModule,
MatAutocompleteModule,
MatChipsModule,
MatListModule,
GridsterModule,
ClipboardModule,
FlexLayoutModule,
@ -358,6 +363,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HotkeyModule,
ColorPickerModule,
NgxHmCarouselModule,
DndModule,
NgxFlowchartModule,
ConfirmDialogComponent,
AlertDialogComponent,

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -299,6 +299,7 @@
"filter-type-single-entity": "Single entity",
"filter-type-entity-list": "Entity list",
"filter-type-entity-name": "Entity name",
"filter-type-entity-type": "Entity type",
"filter-type-state-entity": "Entity from dashboard state",
"filter-type-state-entity-description": "Entity taken from dashboard state parameters",
"filter-type-asset-type": "Asset type",
@ -800,6 +801,7 @@
"datasource": {
"type": "Datasource type",
"name": "Name",
"label": "Label",
"add-datasource-prompt": "Please add datasource"
},
"details": {
@ -1094,6 +1096,7 @@
"entity": {
"entity": "Entity",
"entities": "Entities",
"entities-count": "Entities count",
"aliases": "Entity aliases",
"entity-alias": "Entity alias",
"unable-delete-entity-alias-title": "Unable to delete entity alias",

7
ui-ngx/yarn.lock

@ -6534,6 +6534,13 @@ ngx-daterangepicker-material@^4.0.1:
dependencies:
tslib "^1.10.0"
ngx-drag-drop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ngx-drag-drop/-/ngx-drag-drop-2.0.0.tgz#65d970229964803726fb7b9af4aec24005c810c7"
integrity sha512-t+4/eiC8zaXKqU1ruNfFEfGs1GpMNwpffD0baopvZFKjQHCb5rhNqFilJ54wO4T0OwGp4/RnsVhlcxe1mX6UJg==
dependencies:
tslib "^1.9.0"
"ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master":
version "0.0.0"
resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278"

Loading…
Cancel
Save