Browse Source

Merge remote-tracking branch 'upstream/feature/calculated-fields' into feature/calculated-fields-vc

pull/12707/head
Andrii Landiak 2 years ago
parent
commit
94ec82ea6d
  1. 100
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  2. 28
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  3. 1
      application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldTelemetryMsg.java
  4. 5
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  5. 2
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  6. 1
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java
  7. 5
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java
  8. 6
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java
  10. 28
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  11. 54
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java
  13. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  15. 50
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  16. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  17. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java
  18. 11
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  19. 18
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  20. 72
      application/src/main/java/org/thingsboard/server/service/cf/telemetry/CalculatedFieldAttributeUpdateRequest.java
  21. 39
      application/src/main/java/org/thingsboard/server/service/cf/telemetry/CalculatedFieldTelemetryUpdateRequest.java
  22. 75
      application/src/main/java/org/thingsboard/server/service/cf/telemetry/CalculatedFieldTimeSeriesUpdateRequest.java
  23. 10
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  24. 13
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  25. 5
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  26. 3
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  27. 26
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  28. 8
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java
  29. 2
      application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java
  30. 54
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  31. 5
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  32. 4
      application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java
  33. 2
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  34. 5
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java
  35. 5
      common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java
  36. 15
      common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java
  37. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  38. 6
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java
  39. 31
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java
  40. 4
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java
  41. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java
  42. 2
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  43. 4
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  44. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  45. 14
      common/proto/src/main/proto/queue.proto
  46. 7
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java
  47. 2
      common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java
  48. 18
      common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java
  49. 6
      common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java
  50. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java
  51. 8
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java
  52. 186
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java
  53. 131
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java
  54. 46
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  55. 2
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java
  56. 3
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java
  57. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  58. 20
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java
  59. 14
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
  60. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
  61. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java
  62. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java
  63. 1
      dao/src/main/resources/sql/schema-entities.sql
  64. 7
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  65. 1
      dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java
  66. 2
      pom.xml
  67. 31
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java
  68. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesSaveRequest.java
  69. 21
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldSystemAwareRequest.java
  70. 31
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesDeleteRequest.java
  71. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java
  72. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java
  73. 208
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  74. 54
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss
  75. 125
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts
  76. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  77. 101
      ui-ngx/src/app/shared/components/js-func.component.ts
  78. 5
      ui-ngx/src/app/shared/models/calculated-field.models.ts

100
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -24,11 +24,14 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
@ -56,6 +59,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@ -120,6 +124,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
}
} catch (Exception e) {
if (e instanceof CalculatedFieldException cfe) {
throw cfe;
}
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
}
@ -168,6 +175,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
} else if (proto.getAttrDataCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto));
} else if (proto.getRemovedTsKeysCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} else if (proto.getRemovedAttrKeysCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} else {
callback.onSuccess(CALLBACKS_PER_CF);
}
@ -186,28 +197,40 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
processTelemetry(ctx, proto, cfIdList, callback);
} else if (proto.getAttrDataCount() > 0) {
processAttributes(ctx, proto, cfIdList, callback);
} else if (proto.getRemovedTsKeysCount() > 0) {
processRemovedTelemetry(ctx, proto, cfIdList, callback);
} else if (proto.getRemovedAttrKeysCount() > 0) {
processRemovedAttributes(ctx, proto, cfIdList, callback);
} else {
callback.onSuccess(CALLBACKS_PER_CF);
}
}
} catch (Exception e) {
if (e instanceof CalculatedFieldException cfe) {
throw cfe;
}
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
}
@SneakyThrows
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) {
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
}
@SneakyThrows
private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) {
private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto));
}
@SneakyThrows
private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto));
}
private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
}
private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback,
Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, TbMsgType tbMsgType) {
Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException {
if (newArgValues.isEmpty()) {
log.info("[{}] No new argument values to process for CF.", ctx.getCfId());
callback.onSuccess(CALLBACKS_PER_CF);
@ -249,19 +272,22 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return state;
}
@SneakyThrows
private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) {
private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException {
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
boolean stateSizeOk;
if (ctx.isInitialized() && state.isReady()) {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeOk = state.isSizeOk();
if (stateSizeOk) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null);
try {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeOk = state.isSizeOk();
if (stateSizeOk) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null);
}
}
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
}
} else {
state.checkStateSize(ctxId, ctx.getMaxStateSize());
@ -304,7 +330,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(argNames, data);
}
private static Map<String, ArgumentEntry> mapToArguments(Map<ReferencedEntityKey, String> argNames, List<TsKvProto> data) {
private Map<String, ArgumentEntry> mapToArguments(Map<ReferencedEntityKey, String> argNames, List<TsKvProto> data) {
if (argNames.isEmpty()) {
return Collections.emptyMap();
}
@ -336,7 +362,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(argNames, scope, attrDataList);
}
private static Map<String, ArgumentEntry> mapToArguments(Map<ReferencedEntityKey, String> argNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
private Map<String, ArgumentEntry> mapToArguments(Map<ReferencedEntityKey, String> argNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
Map<String, ArgumentEntry> arguments = new HashMap<>();
for (AttributeValueProto item : attrDataList) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
@ -348,6 +374,46 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return arguments;
}
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List<String> removedAttrKeys) {
var argNames = ctx.getLinkedEntityArguments().get(entityId);
if (argNames.isEmpty()) {
return Collections.emptyMap();
}
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys);
}
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<String> removedAttrKeys) {
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys);
}
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, AttributeScopeProto scope, List<String> removedAttrKeys) {
Map<String, ArgumentEntry> arguments = new HashMap<>();
for (String removedKey : removedAttrKeys) {
ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
String argName = argNames.get(key);
if (argName != null) {
Argument argument = configArguments.get(argName);
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
: new SingleValueArgumentEntry());
}
}
return arguments;
}
private Map<String, ArgumentEntry> mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, List<String> removedTelemetryKeys) {
Map<String, Argument> deletedArguments = ctx.getArguments().entrySet().stream()
.filter(entry -> removedTelemetryKeys.contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Map<String, ArgumentEntry> fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, deletedArguments);
fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true));
return fetchedArgs;
}
private static List<CalculatedFieldId> getCalculatedFieldIds(CalculatedFieldTelemetryMsgProto proto) {
List<CalculatedFieldId> cfIds = new LinkedList<>();
for (var cfId : proto.getPreviousCalculatedFieldsList()) {

28
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
@ -39,7 +38,6 @@ import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache;
@ -53,11 +51,12 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
/**
* @author Andrew Shvayka
@ -66,8 +65,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class CalculatedFieldManagerMessageProcessor extends AbstractContextAwareMsgProcessor {
private final Map<CalculatedFieldId, CalculatedFieldCtx> calculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new ConcurrentHashMap<>();
private final ConcurrentMap<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new ConcurrentHashMap<>();
private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>();
private final CalculatedFieldProcessingService cfExecService;
private final CalculatedFieldStateService cfStateService;
@ -106,7 +105,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new ArrayList<>()).add(cfCtx);
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
msg.getCallback().onSuccess();
}
@ -115,7 +114,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
var link = msg.getLink();
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new ArrayList<>()).add(link);
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link);
msg.getCallback().onSuccess();
}
@ -263,7 +262,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
List<CalculatedFieldCtx> newCfList = new ArrayList<>(oldCfList.size());
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean found = false;
for (CalculatedFieldCtx oldCtx : oldCfList) {
if (oldCtx.getCfId().equals(newCf.getId())) {
@ -300,6 +299,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.warn("[{}] CF was already deleted [{}]", tenantId, cfId);
callback.onSuccess();
} else {
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx);
EntityId entityId = cfCtx.getEntityId();
@ -349,7 +349,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
var proto = msg.getProto();
var linksList = proto.getLinksList();
for (var linkProto : linksList) {
var link = toCalculatedFieldEntityCtxId(linkProto);
var link = fromProto(linkProto);
var targetEntityId = link.entityId();
var targetEntityType = targetEntityId.getEntityType();
var cf = calculatedFields.get(link.cfId());
@ -374,12 +374,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
}
private CalculatedFieldEntityCtxId toCalculatedFieldEntityCtxId(CalculatedFieldEntityCtxIdProto ctxIdProto) {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(ctxIdProto.getEntityType(), new UUID(ctxIdProto.getEntityIdMSB(), ctxIdProto.getEntityIdLSB()));
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID(ctxIdProto.getCalculatedFieldIdMSB(), ctxIdProto.getCalculatedFieldIdLSB()));
return new CalculatedFieldEntityCtxId(tenantId, calculatedFieldId, entityId);
}
private List<CalculatedFieldEntityCtxId> filterCalculatedFieldLinks(CalculatedFieldTelemetryMsg msg) {
EntityId entityId = msg.getEntityId();
var proto = msg.getProto();
@ -462,13 +456,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private void addLinks(CalculatedField newCf) {
var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId());
newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new ArrayList<>()).add(link));
newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link));
}
private void deleteLinks(CalculatedFieldCtx cfCtx) {
var oldCf = cfCtx.getCalculatedField();
var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId());
oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new ArrayList<>()).remove(link));
oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).remove(link));
}
public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) {

1
application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldTelemetryMsg.java

@ -32,6 +32,7 @@ public class EntityCalculatedFieldTelemetryMsg implements ToCalculatedFieldSyste
private final TenantId tenantId;
private final EntityId entityId;
private final CalculatedFieldTelemetryMsgProto proto;
// Both lists are effectively immutable in CalculatedFieldManagerMessageProcessor and must stay so.
private final List<CalculatedFieldCtx> entityIdFields;
private final List<CalculatedFieldCtx> profileIdFields;
private final TbCallback callback;

5
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -35,6 +35,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -53,6 +54,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngi
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Collections;
@ -127,6 +129,7 @@ public class CalculatedFieldController extends BaseController {
public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.")
@RequestBody CalculatedField calculatedField) throws Exception {
calculatedField.setTenantId(getTenantId());
checkEntity(calculatedField.getId(), calculatedField, Resource.CALCULATED_FIELD);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
checkReferencedEntities(calculatedField.getConfiguration(), getCurrentUser());
return tbCalculatedFieldService.save(calculatedField, getCurrentUser());
@ -176,7 +179,7 @@ public class CalculatedFieldController extends BaseController {
public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedField) throws Exception {
checkParameter(CALCULATED_FIELD_ID, strCalculatedField);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedField));
CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.DELETE);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
tbCalculatedFieldService.delete(calculatedField, getCurrentUser());
}

2
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -156,6 +156,8 @@ public class SystemInfoController extends BaseController {
if (debugModeRateLimitsConfig.isCalculatedFieldDebugPerTenantLimitsEnabled()) {
systemParams.setCalculatedFieldDebugPerTenantLimitsConfiguration(debugModeRateLimitsConfig.getCalculatedFieldDebugPerTenantLimitsConfiguration());
}
systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF());
systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg());
}
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))
.map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)

1
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java

@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.List;
import java.util.Set;
public interface CalculatedFieldCache {

5
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java

@ -17,20 +17,25 @@ package org.thingsboard.server.service.cf;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import java.util.List;
import java.util.Map;
public interface CalculatedFieldProcessingService {
ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId);
Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments);
void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List<CalculatedFieldId> cfIds, TbCallback callback);
void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List<CalculatedFieldEntityCtxId> linkedCalculatedFields, TbCallback callback);

6
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java

@ -16,7 +16,9 @@
package org.thingsboard.server.service.cf;
import com.google.common.util.concurrent.FutureCallback;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
@ -34,4 +36,8 @@ public interface CalculatedFieldQueueService {
void pushRequestToQueue(AttributesSaveRequest request, List<Long> result, FutureCallback<Void> callback);
void pushRequestToQueue(AttributesDeleteRequest request, List<String> result, FutureCallback<Void> callback);
void pushRequestToQueue(TimeseriesDeleteRequest request, List<String> result, FutureCallback<Void> callback);
}

2
application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java

@ -19,10 +19,12 @@ import jakarta.annotation.PreDestroy;
import org.rocksdb.Options;
import org.rocksdb.WriteOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.utils.TbRocksDb;
@Component
@ConditionalOnExpression("'${queue.type:null}'=='in-memory'")
public class CfRocksDb extends TbRocksDb {
public CfRocksDb(@Value("${queue.calculated_fields.rocks_db_path:${user.home}/.rocksdb/cf_states}") String path) throws Exception {

28
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java

@ -21,11 +21,9 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
@ -104,10 +102,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
private ListeningExecutorService calculatedFieldCallbackExecutor;
@Value("${calculatedField.initFetchPackSize:50000}")
@Getter
private int initFetchPackSize;
@PostConstruct
public void init() {
calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(
@ -147,6 +141,28 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
}, calculatedFieldCallbackExecutor);
}
@Override
public Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments) {
Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>();
for (var entry : arguments.entrySet()) {
var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId;
var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue());
argFutures.put(entry.getKey(), argValueFuture);
}
return argFutures.entrySet().stream()
.collect(Collectors.toMap(
Entry::getKey, // Keep the key as is
entry -> {
try {
// Resolve the future to get the value
return entry.getValue().get();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e);
}
}
));
}
@Override
public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculatedFieldResult, List<CalculatedFieldId> cfIds, TbCallback callback) {
try {

54
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java

@ -16,12 +16,12 @@
package org.thingsboard.server.service.cf;
import com.google.common.util.concurrent.FutureCallback;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
@ -56,6 +55,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import static org.thingsboard.server.common.util.ProtoUtils.toTsKvProto;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
@Service
@Slf4j
@ -81,17 +81,10 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT
);
@Value("${calculatedField.initFetchPackSize:50000}")
@Getter
private int initFetchPackSize;
@Override
public void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback<Void> callback) {
var tenantId = request.getTenantId();
var entityId = request.getEntityId();
//TODO: 1. check that request entity has calculated fields for entity or profile. If yes - push to corresponding partitions;
//TODO: 2. check that request entity has calculated field links. If yes - push to corresponding partitions;
//TODO: in 1 and 2 we should do the check as quick as possible. Should we also check the field/link keys?;
checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(request.getEntries()), cf -> cf.linkMatches(entityId, request.getEntries()),
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
}
@ -104,6 +97,23 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
}
@Override
public void pushRequestToQueue(AttributesDeleteRequest request, List<String> result, FutureCallback<Void> callback) {
var tenantId = request.getTenantId();
var entityId = request.getEntityId();
checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result, request.getScope()), cf -> cf.linkMatchesAttrKeys(entityId, result, request.getScope()),
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
}
@Override
public void pushRequestToQueue(TimeseriesDeleteRequest request, List<String> result, FutureCallback<Void> callback) {
var tenantId = request.getTenantId();
var entityId = request.getEntityId();
checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result), cf -> cf.linkMatchesTsKeys(entityId, result),
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
}
private void checkEntityAndPushToQueue(TenantId tenantId, EntityId entityId,
Predicate<CalculatedFieldCtx> mainEntityFilter, Predicate<CalculatedFieldCtx> linkedEntityFilter,
Supplier<ToCalculatedFieldMsg> msg, FutureCallback<Void> callback) {
@ -174,6 +184,23 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
return msg.build();
}
private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesDeleteRequest request, List<String> removedKeys) {
CalculatedFieldTelemetryMsgProto telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType())
.setScope(AttributeScopeProto.valueOf(request.getScope().name()))
.addAllRemovedAttrKeys(removedKeys).build();
return ToCalculatedFieldMsg.newBuilder()
.setTelemetryMsg(telemetryMsg)
.build();
}
private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesDeleteRequest request, List<String> removedKeys) {
CalculatedFieldTelemetryMsgProto telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType())
.addAllRemovedTsKeys(removedKeys).build();
return ToCalculatedFieldMsg.newBuilder()
.setTelemetryMsg(telemetryMsg)
.build();
}
private CalculatedFieldTelemetryMsgProto.Builder buildTelemetryMsgProto(TenantId tenantId, EntityId entityId, List<CalculatedFieldId> calculatedFieldIds, UUID tbMsgId, TbMsgType tbMsgType) {
CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = CalculatedFieldTelemetryMsgProto.newBuilder();
@ -202,13 +229,6 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
return telemetryMsg;
}
private CalculatedFieldIdProto toProto(CalculatedFieldId cfId) {
return CalculatedFieldIdProto.newBuilder()
.setCalculatedFieldIdMSB(cfId.getId().getMostSignificantBits())
.setCalculatedFieldIdLSB(cfId.getId().getLeastSignificantBits())
.build();
}
private static TbQueueCallback wrap(FutureCallback<Void> callback) {
if (callback != null) {
return new FutureCallbackWrapper(callback);

2
application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java

@ -38,7 +38,7 @@ import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
//TODO: remove and use TenantEntityProfileCache in each CalculatedFieldManagerMessageProcessor;
//TODO ashvayka: remove and use TenantEntityProfileCache in each CalculatedFieldManagerMessageProcessor;
public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEventListener<PartitionChangeEvent> implements CalculatedFieldEntityProfileCache {
private static final Integer UNKNOWN = 0;

8
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java

@ -44,6 +44,12 @@ public interface ArgumentEntry {
boolean isEmpty();
TbelCfArg toTbelCfArg();
boolean isForceResetPrevious();
void setForceResetPrevious(boolean forceResetPrevious);
static ArgumentEntry createSingleValueArgument(KvEntry kvEntry) {
return new SingleValueArgumentEntry(kvEntry);
}
@ -52,6 +58,4 @@ public interface ArgumentEntry {
return new TsRollingArgumentEntry(kvEntries, limit, timeWindow);
}
TbelCfArg toTbelCfArg();
}

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -55,7 +55,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
ArgumentEntry newEntry = entry.getValue();
ArgumentEntry existingEntry = arguments.get(key);
if (existingEntry == null) {
if (existingEntry == null || newEntry.isForceResetPrevious()) {
validateNewEntry(newEntry);
arguments.put(key, newEntry);
stateUpdated = true;

50
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -169,7 +169,7 @@ public class CalculatedFieldCtx {
return map != null && matchesTimeSeries(map, values);
}
private static boolean matchesAttributes(Map<ReferencedEntityKey, String> argMap, List<AttributeKvEntry> values, AttributeScope scope) {
private boolean matchesAttributes(Map<ReferencedEntityKey, String> argMap, List<AttributeKvEntry> values, AttributeScope scope) {
for (AttributeKvEntry attrKv : values) {
ReferencedEntityKey attrKey = new ReferencedEntityKey(attrKv.getKey(), ArgumentType.ATTRIBUTE, scope);
if (argMap.containsKey(attrKey)) {
@ -193,18 +193,64 @@ public class CalculatedFieldCtx {
return false;
}
public boolean matchesKeys(List<String> keys, AttributeScope scope) {
return matchesAttributesKeys(mainEntityArguments, keys, scope);
}
public boolean matchesKeys(List<String> keys) {
return matchesTimeSeriesKeys(mainEntityArguments, keys);
}
private boolean matchesAttributesKeys(Map<ReferencedEntityKey, String> argMap, List<String> keys, AttributeScope scope) {
for (String key : keys) {
ReferencedEntityKey attrKey = new ReferencedEntityKey(key, ArgumentType.ATTRIBUTE, scope);
if (argMap.containsKey(attrKey)) {
return true;
}
}
return false;
}
private boolean matchesTimeSeriesKeys(Map<ReferencedEntityKey, String> argMap, List<String> keys) {
for (String key : keys) {
ReferencedEntityKey latestKey = new ReferencedEntityKey(key, ArgumentType.TS_LATEST, null);
if (argMap.containsKey(latestKey)) {
return true;
}
ReferencedEntityKey rollingKey = new ReferencedEntityKey(key, ArgumentType.TS_ROLLING, null);
if (argMap.containsKey(rollingKey)) {
return true;
}
}
return false;
}
public boolean linkMatchesAttrKeys(EntityId entityId, List<String> keys, AttributeScope scope) {
var map = linkedEntityArguments.get(entityId);
return map != null && matchesAttributesKeys(map, keys, scope);
}
public boolean linkMatchesTsKeys(EntityId entityId, List<String> keys) {
var map = linkedEntityArguments.get(entityId);
return map != null && matchesTimeSeriesKeys(map, keys);
}
public boolean linkMatches(EntityId entityId, CalculatedFieldTelemetryMsgProto proto) {
if (!proto.getTsDataList().isEmpty()) {
List<TsKvEntry> updatedTelemetry = proto.getTsDataList().stream()
.map(ProtoUtils::fromProto)
.toList();
return linkMatches(entityId, updatedTelemetry);
} else {
} else if (!proto.getAttrDataList().isEmpty()) {
AttributeScope scope = AttributeScope.valueOf(proto.getScope().name());
List<AttributeKvEntry> updatedTelemetry = proto.getAttrDataList().stream()
.map(ProtoUtils::fromProto)
.toList();
return linkMatches(entityId, updatedTelemetry, scope);
} else if (!proto.getRemovedTsKeysList().isEmpty()) {
return linkMatchesTsKeys(entityId, proto.getRemovedTsKeysList());
} else {
return linkMatchesAttrKeys(entityId, proto.getRemovedAttrKeysList(), AttributeScope.valueOf(proto.getScope().name()));
}
}

7
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java

@ -44,15 +44,12 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToUuid;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.stringToBytes;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.uuidToBytes;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.*;
@Service
@RequiredArgsConstructor
@Slf4j
@ConditionalOnExpression("'${queue.type:null}'=='kafka'")
@ConditionalOnExpression("('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-rule-engine') && '${queue.type:null}'=='kafka'")
public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldStateService {
private final TbRuleEngineQueueFactory queueFactory;

7
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java

@ -37,7 +37,7 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS
private final CfRocksDb cfRocksDb;
private Set<TopicPartitionInfo> partitions;
private boolean initialized;
@Override
protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) {
@ -53,7 +53,7 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS
@Override
public void restore(Set<TopicPartitionInfo> partitions) {
if (this.partitions == null) {
if (!this.initialized) {
cfRocksDb.forEach((key, value) -> {
try {
processRestoredState(CalculatedFieldStateProto.parseFrom(value));
@ -61,10 +61,9 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS
log.error("[{}] Failed to process restored state", key, e);
}
});
this.initialized = true;
}
eventConsumer.update(partitions);
this.partitions = partitions;
}
@Override

11
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -38,6 +38,8 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
private BasicKvEntry kvEntryValue;
private Long version;
private boolean forceResetPrevious;
public SingleValueArgumentEntry(TsKvProto entry) {
this.ts = entry.getTs();
this.version = entry.getVersion();
@ -61,6 +63,12 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
this.kvEntryValue = ProtoUtils.basicKvEntryFromKvEntry(entry);
}
public SingleValueArgumentEntry(long ts, BasicKvEntry kvEntryValue, Long version) {
this.ts = ts;
this.kvEntryValue = kvEntryValue;
this.version = version;
}
@Override
public ArgumentEntryType getType() {
return ArgumentEntryType.SINGLE_VALUE;
@ -92,8 +100,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
if (newVersion == null || this.version == null || newVersion > this.version) {
this.ts = singleValueEntry.getTs();
this.version = newVersion;
// TODO: should we persist updated ts and version values?
BasicKvEntry newValue = singleValueEntry.getKvEntryValue();
if (this.kvEntryValue != null && this.kvEntryValue.getValue().equals(newValue.getValue())) {
return false;
@ -101,7 +107,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
this.kvEntryValue = singleValueEntry.getKvEntryValue();
return true;
}
} else {
throw new IllegalArgumentException("Unsupported argument entry type for single value argument entry: " + entry.getType());
}

18
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java

@ -25,7 +25,6 @@ import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.exception.CalculatedFieldStateException;
import java.util.ArrayList;
import java.util.List;
@ -42,6 +41,8 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
private Long timeWindow;
private TreeMap<Long, Double> tsRecords = new TreeMap<>();
private boolean forceResetPrevious;
public TsRollingArgumentEntry(List<TsKvEntry> kvEntries, int limit, long timeWindow) {
this.limit = limit;
this.timeWindow = timeWindow;
@ -60,6 +61,12 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
this.timeWindow = timeWindow;
}
public TsRollingArgumentEntry(Integer limit, Long timeWindow, TreeMap<Long, Double> tsRecords) {
this.limit = limit;
this.timeWindow = timeWindow;
this.tsRecords = tsRecords;
}
@Override
public ArgumentEntryType getType() {
return ArgumentEntryType.TS_ROLLING;
@ -86,7 +93,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
}
@Override
public boolean updateEntry(ArgumentEntry entry) throws CalculatedFieldStateException {
public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof TsRollingArgumentEntry tsRollingEntry) {
updateTsRollingEntry(tsRollingEntry);
} else if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
@ -116,10 +123,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
case STRING -> value.getStrValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString)));
case JSON -> value.getJsonValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString)));
}
cleanupExpiredRecords();
} catch (Exception e) {
log.warn("Time series rolling arguments supports only numeric values.");
// throw new IllegalArgumentException("Time series rolling arguments supports only numeric values.");
tsRecords.put(ts, Double.NaN);
log.debug("Invalid value '{}' for time series rolling arguments. Only numeric values are supported.", value.getValue());
} finally {
cleanupExpiredRecords();
}
}

72
application/src/main/java/org/thingsboard/server/service/cf/telemetry/CalculatedFieldAttributeUpdateRequest.java

@ -1,72 +0,0 @@
/**
* Copyright © 2016-2025 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.cf.telemetry;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
public class CalculatedFieldAttributeUpdateRequest implements CalculatedFieldTelemetryUpdateRequest {
private TenantId tenantId;
private EntityId entityId;
private AttributeScope scope;
private List<? extends KvEntry> kvEntries;
private List<CalculatedFieldId> previousCalculatedFieldIds;
public CalculatedFieldAttributeUpdateRequest(AttributesSaveRequest request) {
this.tenantId = request.getTenantId();
this.entityId = request.getEntityId();
this.scope = request.getScope();
this.kvEntries = request.getEntries();
this.previousCalculatedFieldIds = request.getPreviousCalculatedFieldIds();
}
@Override
public Map<String, KvEntry> getMappedTelemetry(CalculatedFieldCtx ctx, EntityId referencedEntityId) {
Map<String, KvEntry> mappedKvEntries = new HashMap<>();
Map<TbPair<EntityId, ReferencedEntityKey>, String> referencedKeys = ctx.getReferencedEntityKeys();
kvEntries.forEach(entry -> {
String key = entry.getKey();
ReferencedEntityKey referencedEntityKey = new ReferencedEntityKey(key, ArgumentType.ATTRIBUTE, scope);
String argName = referencedKeys.get(new TbPair<>(referencedEntityId, referencedEntityKey));
if (argName != null) {
mappedKvEntries.put(argName, entry);
}
});
return mappedKvEntries;
}
}

39
application/src/main/java/org/thingsboard/server/service/cf/telemetry/CalculatedFieldTelemetryUpdateRequest.java

@ -1,39 +0,0 @@
/**
* Copyright © 2016-2025 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.cf.telemetry;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.List;
import java.util.Map;
public interface CalculatedFieldTelemetryUpdateRequest {
TenantId getTenantId();
EntityId getEntityId();
List<? extends KvEntry> getKvEntries();
List<CalculatedFieldId> getPreviousCalculatedFieldIds();
Map<String, KvEntry> getMappedTelemetry(CalculatedFieldCtx ctx, EntityId referencedEntityId);
}

75
application/src/main/java/org/thingsboard/server/service/cf/telemetry/CalculatedFieldTimeSeriesUpdateRequest.java

@ -1,75 +0,0 @@
/**
* Copyright © 2016-2025 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.cf.telemetry;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
public class CalculatedFieldTimeSeriesUpdateRequest implements CalculatedFieldTelemetryUpdateRequest {
private TenantId tenantId;
private EntityId entityId;
private List<? extends KvEntry> kvEntries;
private List<CalculatedFieldId> previousCalculatedFieldIds;
public CalculatedFieldTimeSeriesUpdateRequest(TimeseriesSaveRequest request) {
this.tenantId = request.getTenantId();
this.entityId = request.getEntityId();
this.kvEntries = request.getEntries();
this.previousCalculatedFieldIds = request.getPreviousCalculatedFieldIds();
}
@Override
public Map<String, KvEntry> getMappedTelemetry(CalculatedFieldCtx ctx, EntityId referencedEntityId) {
Map<String, KvEntry> mappedKvEntries = new HashMap<>();
Map<TbPair<EntityId, ReferencedEntityKey>, String> referencedKeys = ctx.getReferencedEntityKeys();
kvEntries.forEach(entry -> {
String key = entry.getKey();
ReferencedEntityKey tsLatestKey = new ReferencedEntityKey(key, ArgumentType.TS_LATEST, null);
String argTsLatestName = referencedKeys.get(new TbPair<>(referencedEntityId, tsLatestKey));
if (argTsLatestName != null) {
mappedKvEntries.put(argTsLatestName, entry);
} else {
ReferencedEntityKey tsRollingKey = new ReferencedEntityKey(key, ArgumentType.TS_ROLLING, null);
String argTsRollingName = referencedKeys.get(new TbPair<>(referencedEntityId, tsRollingKey));
if (argTsRollingName != null) {
mappedKvEntries.put(argTsRollingName, entry);
}
}
});
return mappedKvEntries;
}
}

10
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -78,16 +78,6 @@ public abstract class AbstractTbEntityService {
@Lazy
private EntitiesVersionControlService vcService;
@Autowired
protected TenantService tenantService;
@Autowired
protected AssetService assetService;
@Autowired
protected DeviceService deviceService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected EntityService entityService;
protected boolean isTestProfile() {

13
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -87,8 +87,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer;
private ListeningExecutorService calculatedFieldsExecutor;
public DefaultTbCalculatedFieldConsumerService(TbRuleEngineQueueFactory tbQueueFactory,
ActorSystemContext actorContext,
TbDeviceProfileCache deviceProfileCache,
@ -109,7 +107,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
@PostConstruct
public void init() {
super.init("tb-cf");
this.calculatedFieldsExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(poolSize, "tb-cf-executor")); // TODO: multiple threads.
this.eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create()
.queueKey(QueueKey.CF)
@ -127,9 +124,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
@PreDestroy
public void destroy() {
super.destroy();
if (calculatedFieldsExecutor != null) {
calculatedFieldsExecutor.shutdownNow();
}
}
@Override
@ -197,10 +191,9 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
packSubmitFuture.cancel(true);
log.info("Timeout to process message: {}", pendingMsgHolder.getMsg());
}
// if (log.isDebugEnabled()) {
// ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
// }
ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process message: {}", id, msg.getValue())); // TODO: replace with commented above after testing
if (log.isDebugEnabled()) {
ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
}
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
}
consumer.commit();

5
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -196,12 +196,12 @@ public class DefaultTbClusterService implements TbClusterService {
UUID msgId = UUID.randomUUID();
TbQueueProducer<TbProtoQueueMsg<ToCalculatedFieldNotificationMsg>> toCfProducer = producerProvider.getCalculatedFieldsNotificationsMsgProducer();
Set<String> tbReServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE);
MultipleTbQueueCallbackWrapper callbackWrapper = new MultipleTbQueueCallbackWrapper(tbReServices.size(), callback);
for (String serviceId : tbReServices) {
TopicPartitionInfo tpi = topicService.getCalculatedFieldNotificationsTopic(serviceId);
toCfProducer.send(tpi, new TbProtoQueueMsg<>(msgId, toCfMsg), null);
toCfProducer.send(tpi, new TbProtoQueueMsg<>(msgId, toCfMsg), callbackWrapper);
toRuleEngineNfs.incrementAndGet();
}
callback.onSuccess(null); // TODO: refactor to be fair, similar to multi-value callback;
}
@Override
@ -697,7 +697,6 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void onAssetUpdated(Asset entity, Asset old) {
var created = old == null;
broadcastEntityChangeToTransport(entity.getTenantId(), entity.getId(), entity, null);
if (old != null) {
boolean assetTypeChanged = !entity.getAssetProfileId().equals(old.getAssetProfileId());
if (assetTypeChanged) {

3
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -51,8 +51,7 @@ public enum Resource {
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS,
CALCULATED_FIELD(EntityType.CALCULATED_FIELD),
CALCULATED_FIELD_LINK(EntityType.CALCULATED_FIELD_LINK);
CALCULATED_FIELD(EntityType.CALCULATED_FIELD);
private final Set<EntityType> entityTypes;

26
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -192,7 +192,9 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override
public void deleteAttributesInternal(AttributesDeleteRequest request) {
ListenableFuture<List<String>> deleteFuture = attrService.removeAll(request.getTenantId(), request.getEntityId(), request.getScope(), request.getKeys());
addMainCallback(deleteFuture, request.getCallback());
DonAsynchron.withCallback(deleteFuture, result -> {
calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback());
}, safeCallback(request.getCallback()), tsCallBackExecutor);
addWsCallback(deleteFuture, success -> onAttributesDelete(request.getTenantId(), request.getEntityId(), request.getScope().name(), request.getKeys(), request.isNotifyDevice()));
}
@ -212,10 +214,14 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
deleteFuture = tsService.remove(request.getTenantId(), request.getEntityId(), request.getDeleteHistoryQueries());
addWsCallback(deleteFuture, result -> onTimeSeriesDelete(request.getTenantId(), request.getEntityId(), request.getKeys(), result));
}
addMainCallback(deleteFuture, __ -> request.getCallback().onSuccess(request.getKeys()), request.getCallback()::onFailure);
DonAsynchron.withCallback(deleteFuture, result -> {
calculatedFieldQueueService.pushRequestToQueue(request, request.getKeys(), getCalculatedFieldCallback(request.getCallback(), request.getKeys()));
}, safeCallback(getCalculatedFieldCallback(request.getCallback(), request.getKeys())), tsCallBackExecutor);
} else {
ListenableFuture<List<String>> deleteFuture = tsService.removeAllLatest(request.getTenantId(), request.getEntityId());
addMainCallback(deleteFuture, request.getCallback()::onSuccess, request.getCallback()::onFailure);
DonAsynchron.withCallback(deleteFuture, result -> {
calculatedFieldQueueService.pushRequestToQueue(request, request.getKeys(), getCalculatedFieldCallback(request.getCallback(), result));
}, safeCallback(getCalculatedFieldCallback(request.getCallback(), request.getKeys())), tsCallBackExecutor);
}
}
@ -344,4 +350,18 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
};
}
private FutureCallback<Void> getCalculatedFieldCallback(FutureCallback<List<String>> originalCallback, List<String> keys) {
return new FutureCallback<Void>() {
@Override
public void onSuccess(Void unused) {
originalCallback.onSuccess(keys);
}
@Override
public void onFailure(Throwable t) {
originalCallback.onFailure(t);
}
};
}
}

8
application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto;
@ -42,6 +43,13 @@ import java.util.UUID;
public class CalculatedFieldUtils {
public static CalculatedFieldIdProto toProto(CalculatedFieldId cfId) {
return CalculatedFieldIdProto.newBuilder()
.setCalculatedFieldIdMSB(cfId.getId().getMostSignificantBits())
.setCalculatedFieldIdLSB(cfId.getId().getLeastSignificantBits())
.build();
}
public static CalculatedFieldEntityCtxIdProto toProto(CalculatedFieldEntityCtxId ctxId) {
return CalculatedFieldEntityCtxIdProto.newBuilder()
.setTenantIdMSB(ctxId.tenantId().getId().getMostSignificantBits())

2
application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java

@ -27,6 +27,7 @@ import org.thingsboard.server.actors.TbActorSystemSettings;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.ruleChain.RuleChainActor;
import org.thingsboard.server.actors.ruleChain.RuleChainToRuleChainMsg;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.RuleChainErrorActor;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.DeviceId;
@ -116,6 +117,7 @@ public class TenantActorTest {
TbActorSystemSettings settings = new TbActorSystemSettings(0, 0, 0);
TbActorSystem system = spy(new DefaultTbActorSystem(settings));
system.createDispatcher(RULE_DISPATCHER_NAME, mock());
system.createDispatcher(DefaultActorService.CF_MANAGER_DISPATCHER_NAME, mock());
TbActorMailbox tenantCtx = new TbActorMailbox(system, settings, null, mock(), mock(), null);
tenantActor.init(tenantCtx);

54
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -83,57 +83,52 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
calculatedField.setConfiguration(config);
calculatedField.setVersion(1L);
// create CF -> perform initial calculation
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0");
});
// update telemetry -> recalculate state
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0");
});
// update CF output -> perform calculation with updated output
Output savedOutput = savedCalculatedField.getConfiguration().getOutput();
savedOutput.setType(OutputType.ATTRIBUTES);
savedOutput.setScope(AttributeScope.SERVER_SCOPE);
savedOutput.setName("temperatureF");
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF");
assertThat(temperatureF).isNotNull();
assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("86.0");
});
// update CF argument -> perform calculation with new argument
Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T");
savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE));
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF");
assertThat(temperatureF).isNotNull();
assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("104.0");
});
// update CF expression -> perform calculation with new expression
savedCalculatedField.getConfiguration().setExpression("1.8 * T + 32");
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF");
assertThat(temperatureF).isNotNull();
@ -168,20 +163,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
calculatedField.setConfiguration(config);
calculatedField.setVersion(1L);
// create CF -> state is not ready -> no calculation performed
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("create CF -> state is not ready -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue();
});
// update telemetry -> perform calculation
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -217,20 +210,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
calculatedField.setConfiguration(config);
calculatedField.setVersion(1L);
// create CF -> perform initial calculation with default value
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("create CF -> perform initial calculation with default value").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6");
});
// update telemetry -> recalculate state
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -283,10 +274,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
calculatedField.setConfiguration(config);
calculatedField.setVersion(1L);
// create CF and perform initial calculation
doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("create CF and perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -299,10 +289,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(z2.get(0).get("value").asText()).isEqualTo("52.0");
});
// update device telemetry -> recalculate state for all assets
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":25}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -315,10 +304,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(z2.get(0).get("value").asText()).isEqualTo("37.0");
});
// update asset 1 telemetry -> recalculate state only for asset 1
doPost("/api/plugins/telemetry/ASSET/" + asset1.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":15}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update asset 1 telemetry -> recalculate state only for asset 1").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -331,10 +319,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(z2.get(0).get("value").asText()).isEqualTo("37.0");
});
// update asset 2 telemetry -> recalculate state only for asset 2
doPost("/api/plugins/telemetry/ASSET/" + asset2.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":5}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update asset 2 telemetry -> recalculate state only for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1 (no changes)
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -347,12 +334,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(z2.get(0).get("value").asText()).isEqualTo("30.0");
});
// add new entity to profile -> calculate state for new entity
Asset asset3 = createAsset("Test asset 3", assetProfile.getId());
doPost("/api/plugins/telemetry/ASSET/" + asset3.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":13}"));
Asset finalAsset3 = asset3;
await().atMost(5, TimeUnit.SECONDS)
await().alias("add new entity to profile -> calculate state for new entity").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 3
ArrayNode z3 = getServerAttributes(finalAsset3.getId(), "z");
@ -360,10 +346,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(z3.get(0).get("value").asText()).isEqualTo("38.0");
});
// update device telemetry -> recalculate state for all assets
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":20}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -386,11 +371,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
asset3.setAssetProfileId(newAssetProfile.getId());
asset3 = doPost("/api/asset", asset3, Asset.class);
// update device telemetry -> recalculate state for asset 1 and asset 2
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":15}"));
Asset updatedAsset3 = asset3;
await().atMost(5, TimeUnit.SECONDS)
await().alias("update device telemetry -> recalculate state for asset 1 and asset 2").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -438,20 +422,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
calculatedField.setConfiguration(config);
calculatedField.setVersion(1L);
// create CF -> ctx is not initialized -> no calculation perform
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().atMost(5, TimeUnit.SECONDS)
await().alias("create CF -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue();
});
// update telemetry -> ctx is not initialized -> no calculation perform
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().atMost(5, TimeUnit.SECONDS)
await().alias("update telemetry -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();

5
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -86,13 +86,14 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig(testDevice.getId()));
assertThat(savedCalculatedField.getVersion()).isEqualTo(calculatedField.getVersion());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField).isEqualTo(savedCalculatedField);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
.andExpect(status().isOk());

4
application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.TbQueueCallback;
import java.util.ArrayList;
import java.util.Collections;
@ -44,6 +45,7 @@ import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -354,7 +356,7 @@ public class TenantProfileControllerTest extends AbstractControllerTest {
argument -> argument.getClass().equals(TenantProfile.class);
if (ComponentLifecycleEvent.DELETED.equals(event)) {
Mockito.verify(tbClusterService, times(cntTime)).onTenantProfileDelete(Mockito.argThat(matcherTenantProfile),
Mockito.isNull());
eq(TbQueueCallback.EMPTY));
testBroadcastEntityStateChangeEventNever(createEntityId_NULL_UUID(new Tenant()));
} else {
Mockito.verify(tbClusterService, times(cntTime)).onTenantProfileChange(Mockito.argThat(matcherTenantProfile),

2
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java

@ -151,7 +151,7 @@ public class ScriptCalculatedFieldStateTest {
}
private TsRollingArgumentEntry createRollingArgEntry() {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry(5, 30000L);
long ts = System.currentTimeMillis();
TreeMap<Long, Double> values = new TreeMap<>();

5
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java

@ -79,9 +79,8 @@ public class TsRollingArgumentEntryTest {
void testUpdateEntryWhenValueIsNotNumber() {
SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new StringDataEntry("key", "string"), 123L);
assertThatThrownBy(() -> entry.updateEntry(newEntry))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Time series rolling arguments supports only numeric values.");
assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.getTsRecords().get(ts - 10)).isNaN();
}
@Test

5
common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java

@ -153,14 +153,13 @@ public final class TbActorMailbox implements TbActorCtx {
}
if (msg != null) {
try {
log.debug("[{}] Going to process message: {}", selfId, msg);
log.trace("[{}] Going to process message: {}", selfId, msg);
actor.process(msg);
} catch (TbRuleNodeUpdateException updateException) {
stopReason = TbActorStopReason.INIT_FAILED;
destroy(updateException.getCause());
} catch (Throwable t) {
//TODO: revert;
log.error("[{}] Failed to process message: {}", selfId, msg, t);
log.debug("[{}] Failed to process message: {}", selfId, msg, t);
ProcessFailureStrategy strategy = actor.onProcessFailure(msg, t);
if (strategy.isStop()) {
system.stop(selfId);

15
common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.cf;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -34,14 +33,10 @@ public interface CalculatedFieldService extends EntityDaoService {
CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId);
ListenableFuture<CalculatedField> findCalculatedFieldByIdAsync(TenantId tenantId, CalculatedFieldId calculatedFieldId);
List<CalculatedFieldId> findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedField> findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedField> findAllCalculatedFields();
PageData<CalculatedField> findAllCalculatedFields(PageLink pageLink);
PageData<CalculatedField> findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink);
@ -54,22 +49,12 @@ public interface CalculatedFieldService extends EntityDaoService {
CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId);
ListenableFuture<CalculatedFieldLink> findCalculatedFieldLinkByIdAsync(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId);
List<CalculatedFieldLink> findAllCalculatedFieldLinks();
List<CalculatedFieldLink> findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId);
List<CalculatedFieldLink> findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
ListenableFuture<List<CalculatedFieldLink>> findAllCalculatedFieldLinksByIdAsync(TenantId tenantId, CalculatedFieldId calculatedFieldId);
PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink);
boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId);
boolean referencedInAnyCalculatedFieldIncludingEntityId(TenantId tenantId, EntityId referencedEntityId);
boolean existsCalculatedFieldByEntityId(TenantId tenantId, EntityId entityId);
}

2
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -35,4 +35,6 @@ public class SystemParams {
int maxDebugModeDurationMinutes;
String ruleChainDebugPerTenantLimitsConfiguration;
String calculatedFieldDebugPerTenantLimitsConfiguration;
long maxArgumentsPerCF;
long maxDataPointsPerRollingArg;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java

@ -36,8 +36,6 @@ public class CalculatedFieldLink extends BaseData<CalculatedFieldLinkId> {
@Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY)
private CalculatedFieldId calculatedFieldId;
@Schema
private transient CalculatedFieldLinkConfiguration configuration;
public CalculatedFieldLink() {
super();
@ -47,11 +45,10 @@ public class CalculatedFieldLink extends BaseData<CalculatedFieldLinkId> {
super(id);
}
public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId, CalculatedFieldLinkConfiguration configuration) {
public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {
this.tenantId = tenantId;
this.entityId = entityId;
this.calculatedFieldId = calculatedFieldId;
this.configuration = configuration;
}
@Override
@ -61,7 +58,6 @@ public class CalculatedFieldLink extends BaseData<CalculatedFieldLinkId> {
.append("tenantId=").append(tenantId)
.append(", entityId=").append(entityId)
.append(", calculatedFieldId=").append(calculatedFieldId)
.append(", configuration=").append(configuration)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')
.toString();

31
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java

@ -17,7 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration;
import lombok.Data;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLinkConfiguration;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -42,35 +41,6 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel
.collect(Collectors.toList());
}
@Override
public CalculatedFieldLinkConfiguration getReferencedEntityConfig(EntityId entityId) {
CalculatedFieldLinkConfiguration linkConfiguration = new CalculatedFieldLinkConfiguration();
arguments.entrySet().stream()
.filter(entry -> entry.getValue().getRefEntityId() != null && entry.getValue().getRefEntityId().equals(entityId))
.forEach(entry -> {
ReferencedEntityKey refEntityKey = entry.getValue().getRefEntityKey();
String argumentName = entry.getKey();
switch (refEntityKey.getType()) {
case ATTRIBUTE -> {
switch (refEntityKey.getScope()) {
case CLIENT_SCOPE ->
linkConfiguration.getClientAttributes().put(refEntityKey.getKey(), argumentName);
case SERVER_SCOPE ->
linkConfiguration.getServerAttributes().put(refEntityKey.getKey(), argumentName);
case SHARED_SCOPE ->
linkConfiguration.getSharedAttributes().put(refEntityKey.getKey(), argumentName);
}
}
case TS_LATEST, TS_ROLLING ->
linkConfiguration.getTimeSeries().put(refEntityKey.getKey(), argumentName);
}
});
return linkConfiguration;
}
@Override
public List<CalculatedFieldLink> buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) {
return getReferencedEntities().stream()
@ -85,7 +55,6 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel
link.setTenantId(tenantId);
link.setEntityId(referencedEntityId);
link.setCalculatedFieldId(calculatedFieldId);
link.setConfiguration(getReferencedEntityConfig(referencedEntityId));
return link;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLinkConfiguration;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
@ -53,9 +52,6 @@ public interface CalculatedFieldConfiguration {
@JsonIgnore
List<EntityId> getReferencedEntities();
@JsonIgnore
CalculatedFieldLinkConfiguration getReferencedEntityConfig(EntityId entityId);
List<CalculatedFieldLink> buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId);
CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId);

2
common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@Schema
public class CalculatedFieldLinkId extends UUIDBased implements EntityId {
public class CalculatedFieldLinkId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;

2
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -44,7 +44,7 @@ public enum LimitedApi {
TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE("transport messages per gateway device", false),
EMAILS("emails sending", true),
WS_SUBSCRIPTIONS("WS subscriptions", false),
CALCULATED_FIELD_DEBUG_EVENTS(DefaultTenantProfileConfiguration::getCalculatedFieldDebugEventsRateLimit, "calculated field debug events", true);
CALCULATED_FIELD_DEBUG_EVENTS("calculated field debug events", true);
private Function<DefaultTenantProfileConfiguration, String> configExtractor;
@Getter

4
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -135,12 +135,11 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private double warnThreshold;
private long maxCalculatedFields;
private long maxCalculatedFieldsPerEntity;
private long maxArgumentsPerCF;
private long maxDataPointsPerRollingArg;
private long maxStateSizeInKBytes;
private long maxSingleValueArgumentSizeInKBytes;
private String calculatedFieldDebugEventsRateLimit;
@Override
public long getProfileThreshold(ApiUsageRecordKey key) {
@ -182,7 +181,6 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
case DASHBOARD -> maxDashboards;
case RULE_CHAIN -> maxRuleChains;
case EDGE -> maxEdges;
case CALCULATED_FIELD -> maxCalculatedFields;
default -> 0;
};
}

2
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -485,7 +485,7 @@ public final class TbMsg implements Serializable {
}
public TbMsgBuilder previousCalculatedFieldIds(List<CalculatedFieldId> previousCalculatedFieldIds) {
this.previousCalculatedFieldIds = previousCalculatedFieldIds;
this.previousCalculatedFieldIds = new CopyOnWriteArrayList<>(previousCalculatedFieldIds);
return this;
}

14
common/proto/src/main/proto/queue.proto

@ -783,9 +783,11 @@ message CalculatedFieldTelemetryMsgProto {
repeated TsKvProto tsData = 9;
AttributeScopeProto scope = 10;
repeated AttributeValueProto attrData = 11;
int64 tbMsgIdMSB = 12;
int64 tbMsgIdLSB = 13;
string tbMsgType = 14;
repeated string removedTsKeys = 12;
repeated string removedAttrKeys = 13;
int64 tbMsgIdMSB = 14;
int64 tbMsgIdLSB = 15;
string tbMsgType = 16;
}
message CalculatedFieldLinkedTelemetryMsgProto {
@ -1188,9 +1190,9 @@ message ComponentLifecycleMsgProto {
int64 entityIdMSB = 4;
int64 entityIdLSB = 5;
ComponentLifecycleEvent event = 6;
//Since 4.0. To replace the
optional string oldName = 7;
optional string name = 8;
//Since 4.0. TODO: replace the DeviceNameOrTypeUpdateMsgProto
string oldName = 7;
string name = 8;
int64 oldProfileIdMSB = 9;
int64 oldProfileIdLSB = 10;
int64 profileIdMSB = 11;

7
common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java

@ -39,12 +39,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.SystemUtil.getCpuCount;
import static org.thingsboard.common.util.SystemUtil.getCpuUsage;
import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage;
import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getTotalMemory;
import static org.thingsboard.common.util.SystemUtil.*;
@Component

2
common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java

@ -15,8 +15,8 @@
*/
package org.thingsboard.server.queue.provider;
import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg;

18
common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java

@ -37,23 +37,11 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.BDDMockito.*;
import static org.mockito.Mockito.*;
import static org.mockito.hamcrest.MockitoHamcrest.longThat;
@Slf4j

6
common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java

@ -42,11 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class ZkDiscoveryServiceTest {

2
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.script.api.tbel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@ -29,6 +30,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
})
public interface TbelCfArg extends TbelCfObject {
@JsonIgnore
String getType();
}

8
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java

@ -22,6 +22,8 @@ import lombok.Data;
@Data
public class TbelCfSingleValueArg implements TbelCfArg {
public static final long OBJ_SIZE = 32L; // Approximate calculation;
private final long ts;
private final Object value;
@ -36,7 +38,11 @@ public class TbelCfSingleValueArg implements TbelCfArg {
@Override
public long memorySize() {
return 8L; // TODO;
if (value instanceof String strValue) {
return OBJ_SIZE + strValue.length();
} else {
return OBJ_SIZE;
}
}
@Override

186
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@ -60,9 +61,20 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
}
public double max() {
return max(true);
}
public double max(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
double max = Double.MIN_VALUE;
for (TbelCfTsDoubleVal value : values) {
double val = value.getValue();
if (!ignoreNaN && Double.isNaN(val)) {
return val;
}
if (max < val) {
max = val;
}
@ -70,6 +82,180 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
return max;
}
public double min() {
return min(true);
}
public double min(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
double min = Double.MAX_VALUE;
for (TbelCfTsDoubleVal value : values) {
double val = value.getValue();
if (!ignoreNaN && Double.isNaN(val)) {
return Double.NaN;
}
if (min > val) {
min = val;
}
}
return min;
}
public double mean() {
return mean(true);
}
public double mean(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
return sum(ignoreNaN) / count(ignoreNaN);
}
public double std() {
return std(true);
}
public double std(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
double mean = mean(ignoreNaN);
if (!ignoreNaN && Double.isNaN(mean)) {
return Double.NaN;
}
double sum = 0;
for (TbelCfTsDoubleVal value : values) {
double val = value.getValue();
if (Double.isNaN(val)) {
if (!ignoreNaN) {
return Double.NaN;
}
} else {
sum += Math.pow(val - mean, 2);
}
}
return Math.sqrt(sum / count(ignoreNaN));
}
public double median() {
return median(true);
}
public double median(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
List<Double> sortedValues = new ArrayList<>();
for (TbelCfTsDoubleVal value : values) {
double val = value.getValue();
if (Double.isNaN(val)) {
if (!ignoreNaN) {
return Double.NaN;
}
} else {
sortedValues.add(val);
}
}
Collections.sort(sortedValues);
int size = sortedValues.size();
return (size % 2 == 1)
? sortedValues.get(size / 2)
: (sortedValues.get(size / 2 - 1) + sortedValues.get(size / 2)) / 2.0;
}
public int count() {
return count(true);
}
public int count(boolean ignoreNaN) {
int count = 0;
if (ignoreNaN) {
for (TbelCfTsDoubleVal value : values) {
if (!Double.isNaN(value.getValue())) {
count++;
}
}
return count;
}
return values.size();
}
public double last() {
return last(true);
}
public double last(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
double value = values.get(values.size() - 1).getValue();
if (!Double.isNaN(value) || !ignoreNaN) {
return value;
}
for (int i = values.size() - 2; i >= 0; i--) {
double prevValue = values.get(i).getValue();
if (!Double.isNaN(prevValue)) {
return prevValue;
}
}
throw new IllegalArgumentException("Rolling argument values are empty.");
}
public double first() {
return first(true);
}
public double first(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
double firstValue = values.get(0).getValue();
if (!Double.isNaN(firstValue) || !ignoreNaN) {
return firstValue;
}
for (int i = 1; i < values.size(); i++) {
double nextValue = values.get(i).getValue();
if (!Double.isNaN(nextValue)) {
return nextValue;
}
}
throw new IllegalArgumentException("Rolling argument values are empty.");
}
public double sum() {
return sum(true);
}
public double sum(boolean ignoreNaN) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Rolling argument values are empty.");
}
double sum = 0;
for (TbelCfTsDoubleVal value : values) {
double val = value.getValue();
if (Double.isNaN(val)) {
if (!ignoreNaN) {
return Double.NaN;
}
} else {
sum += val;
}
}
return sum;
}
@JsonIgnore
public int getSize() {
return values.size();

131
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java

@ -0,0 +1,131 @@
/**
* Copyright © 2016-2025 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.script.api.tbel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.within;
public class TbelCfTsRollingArgTest {
private final long ts = System.currentTimeMillis();
private TbelCfTsRollingArg rollingArg;
@BeforeEach
void setUp() {
rollingArg = new TbelCfTsRollingArg(
new TbTimeWindow(ts - 30000, ts - 10, 10),
List.of(
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
new TbelCfTsDoubleVal(ts - 20, 2.0),
new TbelCfTsDoubleVal(ts - 30, 8.0),
new TbelCfTsDoubleVal(ts - 40, Double.NaN),
new TbelCfTsDoubleVal(ts - 50, 3.0),
new TbelCfTsDoubleVal(ts - 60, 9.0),
new TbelCfTsDoubleVal(ts - 70, Double.NaN)
)
);
}
@Test
void testMax() {
assertThat(rollingArg.max()).isEqualTo(9.0);
assertThat(rollingArg.max(false)).isNaN();
}
@Test
void testMin() {
assertThat(rollingArg.min()).isEqualTo(2.0);
assertThat(rollingArg.min(false)).isNaN();
}
@Test
void testMean() {
assertThat(rollingArg.mean()).isEqualTo(5.5);
assertThat(rollingArg.mean(false)).isNaN();
}
@Test
void testStd() {
assertThat(rollingArg.std()).isCloseTo(3.0413812651491097, within(0.001));
assertThat(rollingArg.std(false)).isNaN();
}
@Test
void testMedian() {
assertThat(rollingArg.median()).isEqualTo(5.5);
assertThat(rollingArg.median(false)).isNaN();
}
@Test
void testCount() {
assertThat(rollingArg.count()).isEqualTo(4);
assertThat(rollingArg.count(false)).isEqualTo(7);
}
@Test
void testLast() {
assertThat(rollingArg.last()).isEqualTo(9.0);
assertThat(rollingArg.last(false)).isNaN();
}
@Test
void testFirst() {
assertThat(rollingArg.first()).isEqualTo(2.0);
assertThat(rollingArg.first(false)).isNaN();
}
@Test
void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() {
assertThat(rollingArg.first()).isEqualTo(2.0);
rollingArg = new TbelCfTsRollingArg(
new TbTimeWindow(ts - 30000, ts - 10, 10),
List.of(
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
new TbelCfTsDoubleVal(ts - 40, Double.NaN),
new TbelCfTsDoubleVal(ts - 70, Double.NaN)
)
);
assertThatThrownBy(rollingArg::first).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
}
@Test
void testSum() {
assertThat(rollingArg.sum()).isEqualTo(22.0);
assertThat(rollingArg.sum(false)).isNaN();
}
@Test
void testEmptyValues() {
rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10, 10), List.of());
assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::mean).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::std).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::median).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::first).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
}
}

46
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -84,13 +84,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFieldDao.findById(tenantId, calculatedFieldId.getId());
}
@Override
public ListenableFuture<CalculatedField> findCalculatedFieldByIdAsync(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
log.trace("Executing findCalculatedFieldByIdAsync [{}]", calculatedFieldId);
validateId(calculatedFieldId, id -> INCORRECT_CALCULATED_FIELD_ID + id);
return calculatedFieldDao.findByIdAsync(tenantId, calculatedFieldId.getId());
}
@Override
public List<CalculatedFieldId> findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId) {
log.trace("Executing findCalculatedFieldIdsByEntityId [{}]", entityId);
@ -105,12 +98,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFieldDao.findCalculatedFieldsByEntityId(tenantId, entityId);
}
@Override
public List<CalculatedField> findAllCalculatedFields() {
log.trace("Executing findAll");
return calculatedFieldDao.findAll();
}
@Override
public PageData<CalculatedField> findAllCalculatedFields(PageLink pageLink) {
log.trace("Executing findAll, pageLink [{}]", pageLink);
@ -176,20 +163,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId());
}
@Override
public ListenableFuture<CalculatedFieldLink> findCalculatedFieldLinkByIdAsync(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) {
log.trace("Executing findCalculatedFieldLinkByIdAsync [{}]", calculatedFieldLinkId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id);
return calculatedFieldLinkDao.findByIdAsync(tenantId, calculatedFieldLinkId.getId());
}
@Override
public List<CalculatedFieldLink> findAllCalculatedFieldLinks() {
log.trace("Executing findAllCalculatedFieldLinks");
return calculatedFieldLinkDao.findAll();
}
@Override
public List<CalculatedFieldLink> findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
log.trace("Executing findAllCalculatedFieldLinksById, calculatedFieldId [{}]", calculatedFieldId);
@ -202,12 +175,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId);
}
@Override
public ListenableFuture<List<CalculatedFieldLink>> findAllCalculatedFieldLinksByIdAsync(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
log.trace("Executing findAllCalculatedFieldLinksByIdAsync, calculatedFieldId [{}]", calculatedFieldId);
return calculatedFieldLinkDao.findCalculatedFieldLinksByCalculatedFieldIdAsync(tenantId, calculatedFieldId);
}
@Override
public PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink) {
log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink);
@ -224,19 +191,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
.anyMatch(referencedEntities -> referencedEntities.contains(referencedEntityId));
}
@Override
public boolean referencedInAnyCalculatedFieldIncludingEntityId(TenantId tenantId, EntityId referencedEntityId) {
return calculatedFieldDao.findAllByTenantId(tenantId).stream()
.map(CalculatedField::getConfiguration)
.map(CalculatedFieldConfiguration::getReferencedEntities)
.anyMatch(referencedEntities -> referencedEntities.contains(referencedEntityId));
}
@Override
public boolean existsCalculatedFieldByEntityId(TenantId tenantId, EntityId entityId) {
return calculatedFieldDao.existsByEntityId(tenantId, entityId);
}
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findById(tenantId, new CalculatedFieldId(entityId.getId())));

2
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java

@ -42,8 +42,6 @@ public interface CalculatedFieldDao extends Dao<CalculatedField>, ExportableEnti
List<CalculatedField> removeAllByEntityId(TenantId tenantId, EntityId entityId);
boolean existsByEntityId(TenantId tenantId, EntityId entityId);
long countCFByEntityId(TenantId tenantId, EntityId entityId);
}

3
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.cf;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
@ -32,8 +31,6 @@ public interface CalculatedFieldLinkDao extends Dao<CalculatedFieldLink> {
List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
ListenableFuture<List<CalculatedFieldLink>> findCalculatedFieldLinksByCalculatedFieldIdAsync(TenantId tenantId, CalculatedFieldId calculatedFieldId);
List<CalculatedFieldLink> findAll();
PageData<CalculatedFieldLink> findAll(PageLink pageLink);

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -739,7 +739,6 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_LINK_ENTITY_TYPE = ENTITY_TYPE_COLUMN;
public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN;
public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id";
public static final String CALCULATED_FIELD_LINK_CONFIGURATION = "configuration";
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};

20
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java

@ -22,22 +22,17 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLinkConfiguration;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CONFIGURATION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME;
@ -61,24 +56,10 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink
@Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID)
private UUID calculatedFieldId;
@Convert(converter = JsonConverter.class)
@Column(name = CALCULATED_FIELD_LINK_CONFIGURATION)
private JsonNode configuration;
public CalculatedFieldLinkEntity() {
super();
}
public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) {
this.setUuid(calculatedFieldLink.getUuidId());
this.createdTime = calculatedFieldLink.getCreatedTime();
this.tenantId = calculatedFieldLink.getTenantId().getId();
this.entityType = calculatedFieldLink.getEntityId().getEntityType().name();
this.entityId = calculatedFieldLink.getEntityId().getId();
this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId();
this.configuration = JacksonUtil.valueToTree(calculatedFieldLink.getConfiguration());
}
@Override
public CalculatedFieldLink toData() {
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id));
@ -86,7 +67,6 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink
calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId));
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId));
calculatedFieldLink.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldLinkConfiguration.class));
return calculatedFieldLink;
}

14
dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java

@ -17,8 +17,8 @@ package org.thingsboard.server.dao.service.validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.cf.CalculatedFieldDao;
@ -37,7 +37,7 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
@Override
protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) {
validateNumberOfEntitiesPerTenant(tenantId, EntityType.CALCULATED_FIELD);
validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId());
validateNumberOfArgumentsPerCF(tenantId, calculatedField);
}
@ -51,6 +51,16 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
return old;
}
private void validateNumberOfCFsPerEntity(TenantId tenantId, EntityId entityId) {
long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity);
if (maxCFsPerEntity <= 0) {
return;
}
if (calculatedFieldDao.countCFByEntityId(tenantId, entityId) >= maxCFsPerEntity) {
throw new DataValidationException("Calculated fields per entity limit reached!");
}
}
private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) {
long maxArgumentsPerCF = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxArgumentsPerCF);
if (maxArgumentsPerCF <= 0) {

2
dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java

@ -26,7 +26,6 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLinkConfiguration;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
@ -128,7 +127,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
calculatedFieldLink.setTenantId(new TenantId(tenantId));
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId));
calculatedFieldLink.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldLinkConfiguration.class));
return calculatedFieldLink;
}).collect(Collectors.toList());

5
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java

@ -83,11 +83,6 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
return DaoUtil.convertDataList(calculatedFieldRepository.removeAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId()));
}
@Override
public boolean existsByEntityId(TenantId tenantId, EntityId entityId) {
return calculatedFieldRepository.existsByTenantIdAndEntityId(tenantId.getId(), entityId.getId());
}
@Override
public long countCFByEntityId(TenantId tenantId, EntityId entityId) {
return calculatedFieldRepository.countByTenantIdAndEntityId(tenantId.getId(), entityId.getId());

5
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java

@ -55,11 +55,6 @@ public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLin
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId()));
}
@Override
public ListenableFuture<List<CalculatedFieldLink>> findCalculatedFieldLinksByCalculatedFieldIdAsync(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
return service.submit(() -> findCalculatedFieldLinksByCalculatedFieldId(tenantId, calculatedFieldId));
}
@Override
public List<CalculatedFieldLink> findAll() {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll());

1
dao/src/main/resources/sql/schema-entities.sql

@ -947,7 +947,6 @@ CREATE TABLE IF NOT EXISTS calculated_field_link (
entity_type VARCHAR(32),
entity_id uuid NOT NULL,
calculated_field_id uuid NOT NULL,
configuration varchar(10000),
CONSTRAINT fk_calculated_field_id FOREIGN KEY (calculated_field_id) REFERENCES calculated_field(id) ON DELETE CASCADE
);

7
dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -78,14 +78,14 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
assertThat(savedCalculatedField.getVersion()).isEqualTo(calculatedField.getVersion());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField);
savedCalculatedField.setVersion(savedCalculatedField.getVersion() + 1);
assertThat(updatedCalculatedField).isEqualTo(savedCalculatedField);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId());
}
@ -148,7 +148,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setVersion(1L);
return calculatedField;
}

1
dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java

@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.cf.CalculatedFieldDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.dao.usagerecord.DefaultApiLimitService;
import java.util.UUID;

2
pom.xml

@ -166,7 +166,7 @@
<drewnoakes-metadata-extractor.version>2.19.0</drewnoakes-metadata-extractor.version>
<firebase-admin.version>9.2.0</firebase-admin.version>
<rocksdbjni.version>9.4.0</rocksdbjni.version>
<rocksdbjni.version>9.10.0</rocksdbjni.version>
</properties>
<modules>

31
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java

@ -22,21 +22,27 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import java.util.List;
import java.util.UUID;
@Getter
@ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AttributesDeleteRequest {
public class AttributesDeleteRequest implements CalculatedFieldSystemAwareRequest {
private final TenantId tenantId;
private final EntityId entityId;
private final AttributeScope scope;
private final List<String> keys;
private final boolean notifyDevice;
private final List<CalculatedFieldId> previousCalculatedFieldIds;
private final UUID tbMsgId;
private final TbMsgType tbMsgType;
private final FutureCallback<Void> callback;
public static Builder builder() {
@ -50,9 +56,13 @@ public class AttributesDeleteRequest {
private AttributeScope scope;
private List<String> keys;
private boolean notifyDevice;
private List<CalculatedFieldId> previousCalculatedFieldIds;
private UUID tbMsgId;
private TbMsgType tbMsgType;
private FutureCallback<Void> callback;
Builder() {}
Builder() {
}
public Builder tenantId(TenantId tenantId) {
this.tenantId = tenantId;
@ -89,6 +99,21 @@ public class AttributesDeleteRequest {
return this;
}
public Builder previousCalculatedFieldIds(List<CalculatedFieldId> previousCalculatedFieldIds) {
this.previousCalculatedFieldIds = previousCalculatedFieldIds;
return this;
}
public Builder tbMsgId(UUID tbMsgId) {
this.tbMsgId = tbMsgId;
return this;
}
public Builder tbMsgType(TbMsgType tbMsgType) {
this.tbMsgType = tbMsgType;
return this;
}
public Builder callback(FutureCallback<Void> callback) {
this.callback = callback;
return this;
@ -109,7 +134,7 @@ public class AttributesDeleteRequest {
}
public AttributesDeleteRequest build() {
return new AttributesDeleteRequest(tenantId, entityId, scope, keys, notifyDevice, callback);
return new AttributesDeleteRequest(tenantId, entityId, scope, keys, notifyDevice, previousCalculatedFieldIds, tbMsgId, tbMsgType, callback);
}
}

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesSaveRequest.java

@ -36,7 +36,7 @@ import java.util.UUID;
@Getter
@ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AttributesSaveRequest {
public class AttributesSaveRequest implements CalculatedFieldSystemAwareRequest {
private final TenantId tenantId;
private final EntityId entityId;

21
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLinkConfiguration.java → rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/CalculatedFieldSystemAwareRequest.java

@ -13,19 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf;
package org.thingsboard.rule.engine.api;
import lombok.Data;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.UUID;
@Data
public class CalculatedFieldLinkConfiguration {
public interface CalculatedFieldSystemAwareRequest {
private Map<String, String> clientAttributes = new HashMap<>();
private Map<String, String> serverAttributes = new HashMap<>();
private Map<String, String> sharedAttributes = new HashMap<>();
private Map<String, String> timeSeries = new HashMap<>();
List<CalculatedFieldId> getPreviousCalculatedFieldIds();
UUID getTbMsgId();
TbMsgType getTbMsgType();
}

31
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesDeleteRequest.java

@ -20,21 +20,27 @@ import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.msg.TbMsgType;
import java.util.List;
import java.util.UUID;
@Getter
@ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class TimeseriesDeleteRequest {
public class TimeseriesDeleteRequest implements CalculatedFieldSystemAwareRequest {
private final TenantId tenantId;
private final EntityId entityId;
private final List<String> keys;
private final List<DeleteTsKvQuery> deleteHistoryQueries;
private final List<CalculatedFieldId> previousCalculatedFieldIds;
private final UUID tbMsgId;
private final TbMsgType tbMsgType;
private final FutureCallback<List<String>> callback;
public static Builder builder() {
@ -47,9 +53,13 @@ public class TimeseriesDeleteRequest {
private EntityId entityId;
private List<String> keys;
private List<DeleteTsKvQuery> deleteHistoryQueries;
private List<CalculatedFieldId> previousCalculatedFieldIds;
private UUID tbMsgId;
private TbMsgType tbMsgType;
private FutureCallback<List<String>> callback;
Builder() {}
Builder() {
}
public Builder tenantId(TenantId tenantId) {
this.tenantId = tenantId;
@ -71,13 +81,28 @@ public class TimeseriesDeleteRequest {
return this;
}
public Builder previousCalculatedFieldIds(List<CalculatedFieldId> previousCalculatedFieldIds) {
this.previousCalculatedFieldIds = previousCalculatedFieldIds;
return this;
}
public Builder tbMsgId(UUID tbMsgId) {
this.tbMsgId = tbMsgId;
return this;
}
public Builder tbMsgType(TbMsgType tbMsgType) {
this.tbMsgType = tbMsgType;
return this;
}
public Builder callback(FutureCallback<List<String>> callback) {
this.callback = callback;
return this;
}
public TimeseriesDeleteRequest build() {
return new TimeseriesDeleteRequest(tenantId, entityId, keys, deleteHistoryQueries, callback);
return new TimeseriesDeleteRequest(tenantId, entityId, keys, deleteHistoryQueries, previousCalculatedFieldIds, tbMsgId, tbMsgType, callback);
}
}

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java

@ -37,7 +37,7 @@ import static java.util.Objects.requireNonNullElse;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class TimeseriesSaveRequest {
public class TimeseriesSaveRequest implements CalculatedFieldSystemAwareRequest {
private final TenantId tenantId;
private final CustomerId customerId;

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java

@ -76,6 +76,9 @@ public class TbMsgDeleteAttributesNode implements TbNode {
.scope(scope)
.keys(keysToDelete)
.notifyDevice(checkNotifyDevice(msg.getMetaData().getValue(NOTIFY_DEVICE_METADATA_KEY), scope))
.previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds())
.tbMsgId(msg.getId())
.tbMsgType(msg.getInternalType())
.callback(config.isSendAttributesDeletedNotification() ?
new AttributesDeleteNodeCallback(ctx, msg, scope.name(), keysToDelete) :
new TelemetryNodeCallback(ctx, msg))

208
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html

@ -16,108 +16,112 @@
-->
<div class="flex flex-col gap-3">
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="argument-name-field tb-form-table-header-cell w-1/6 xs:w-1/3 sm:w-1/4" tbTruncateWithTooltip>{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-form-table-header-cell w-1/3 xs:hidden">{{ 'calculated-fields.datasource' | translate }}</div>
<div class="tb-form-table-header-cell w-1/6 lt-md:hidden">{{ 'common.type' | translate }}</div>
<div class="tb-form-table-header-cell w-1/6 xs:w-1/3">{{ 'entity.key' | translate }}</div>
<div class="tb-form-table-header-cell w-20 min-w-20"></div>
</div>
<div class="tb-form-table-body">
@for (group of argumentsFormArray.controls; track group) {
<div [formGroup]="group" class="tb-form-table-row">
<mat-form-field appearance="outline" class="argument-name-field tb-inline-field w-1/6 xs:w-1/3 sm:w-1/4" subscriptSizing="dynamic">
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
</mat-form-field>
<section class="datasource-field flex w-1/3 gap-2 xs:hidden">
@if (group.get('refEntityId')?.get('id')?.value) {
<ng-container [formGroup]="group.get('refEntityId')">
<mat-form-field appearance="outline" class="tb-inline-field w-1/2" subscriptSizing="dynamic">
<mat-select [value]="group.get('refEntityId').get('entityType').value" formControlName="entityType">
<mat-option [value]="group.get('refEntityId').get('entityType').value">
{{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-entity-autocomplete
class="entity-field w-1/2"
formControlName="id"
[inlineField]="true"
[hideLabel]="true"
[placeholder]="'action.set' | translate"
[entityType]="group.get('refEntityId').get('entityType').value"
/>
</ng-container>
} @else {
<mat-form-field appearance="outline" class="tb-inline-field flex-1" subscriptSizing="dynamic">
<mat-select [value]="'current'" [disabled]="true">
<mat-option [value]="'current'">
{{
(group.get('refEntityId')?.get('entityType')?.value === ArgumentEntityType.Tenant
? 'calculated-fields.argument-current-tenant'
: 'calculated-fields.argument-current') | translate
}}
</mat-option>
</mat-select>
</mat-form-field>
}
</section>
<ng-container [formGroup]="group.get('refEntityKey')">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6 lt-md:hidden" subscriptSizing="dynamic">
@if (group.get('refEntityKey').get('type').value; as type) {
<mat-select [value]="type" formControlName="type">
<mat-option [value]="type">
{{ ArgumentTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
}
</mat-form-field>
<mat-chip-listbox formControlName="key" class="tb-inline-field entity-key-field w-1/6 xs:w-1/3">
<mat-chip>
<div tbTruncateWithTooltip>
{{ group.get('refEntityKey').get('key').value }}
</div>
</mat-chip>
</mat-chip-listbox>
</ng-container>
<div class="tb-form-table-row-cell-buttons flex w-20 min-w-20">
<button type="button"
mat-icon-button
#button
(click)="manageArgument($event, button, $index)"
[matTooltip]="'action.edit' | translate"
matTooltipPosition="above">
<mat-icon
[matBadgeHidden]="!(group.get('refEntityKey').get('type').value === ArgumentType.Rolling
&& calculatedFieldType === CalculatedFieldType.SIMPLE)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button type="button"
mat-icon-button
(click)="onDelete($index)"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
} @empty {
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-arguments' | translate }}</span>
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col" [class.arguments-table-with-error]="errorText">
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="!w-1/4 xs:!w-1/2">
<div tbTruncateWithTooltip>{{ 'common.name' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let argument" class="w-1/4 xs:w-1/2">
<div tbTruncateWithTooltip>{{ argument.argumentName }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'entityType'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="entity-type-header w-1/4 xs:hidden">
{{ 'entity.entity-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let argument" class="w-1/4 xs:hidden">
<div tbTruncateWithTooltip>
@if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) {
{{ 'calculated-fields.argument-current-tenant' | translate }}
} @else if (argument.refEntityId?.id) {
{{ entityTypeTranslations.get(argument.refEntityId.entityType).type | translate }}
} @else {
{{ 'calculated-fields.argument-current' | translate }}
}
</div>
@if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="pl-3"/>
}
</div>
<div>
<button type="button" mat-stroked-button color="primary" #button (click)="manageArgument($event, button)">
{{ 'calculated-fields.add-argument' | translate }}
</button>
</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'target'">
<mat-header-cell *matHeaderCellDef class="w-1/4 xs:hidden">
{{ 'entity-view.target-entity' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let argument" class="w-1/4 xs:hidden">
<div tbTruncateWithTooltip>
@if (argument.refEntityId?.id) {
<a aria-label="Open entity details page"
[routerLink]="getEntityDetailsPageURL(argument.refEntityId.id, argument.refEntityId.entityType)">
{{ entityNameMap.get(argument.refEntityId.id) ?? '' }}
</a>
}
</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="w-1/4 lt-md:hidden">
{{ 'common.type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let argument" class="w-1/4 lt-md:hidden">
<div tbTruncateWithTooltip>{{ ArgumentTypeTranslations.get(argument.refEntityKey.type) | translate }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'key'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="w-1/4 xs:w-1/3">
{{ 'entity.key' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let argument" class="w-1/4 xs:w-1/3">
<mat-chip>
<div tbTruncateWithTooltip class="key-text">{{ argument.refEntityKey.key }}</div>
</mat-chip>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef class="w-20 min-w-20"/>
<mat-cell *matCellDef="let argument; let $index = index">
<div class="tb-form-table-row-cell-buttons flex w-20 min-w-20">
<button type="button"
mat-icon-button
#button
(click)="manageArgument($event, button, argument, $index)"
[matTooltip]="'action.edit' | translate"
matTooltipPosition="above">
<mat-icon
[matBadgeHidden]="!(argument.refEntityKey.type === ArgumentType.Rolling
&& calculatedFieldType === CalculatedFieldType.SIMPLE)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button type="button"
mat-icon-button
(click)="onDelete($event, $index)"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select"
*matHeaderRowDef="['name', 'entityType', 'target', 'type', 'key', 'actions']; sticky: true"></mat-header-row>
<mat-row
*matRowDef="let argument; columns: ['name', 'entityType', 'target', 'type', 'key', 'actions']"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
@if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div>
<button type="button" mat-stroked-button color="primary" #button (click)="manageArgument($event, button)">
{{ 'calculated-fields.add-argument' | translate }}
</button>
</div>
</div>

54
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss

@ -13,50 +13,46 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@use '../../../../../../../scss/constants' as constants;
:host {
.entity-key-field {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.arguments-table {
min-height: 108px;
.tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0;
}
&-with-error {
min-height: 150px;
}
.argument-name-field {
@media #{constants.$mat-sm} {
min-width: 25%;
max-width: 25%;
.mat-mdc-table {
table-layout: fixed;
}
@media #{constants.$mat-xs} {
min-width: 33%;
max-width: 33%;
.key-text {
font-size: 13px;
}
}
.datasource-field {
min-width: 33%;
max-width: 33%;
.tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0;
}
}
:host ::ng-deep {
.entity-field {
a {
font-size: 14px;
white-space: nowrap;
display: block;
.mat-mdc-standard-chip {
.mdc-evolution-chip__cell--primary, .mdc-evolution-chip__action--primary, .mdc-evolution-chip__text-label {
overflow: hidden;
text-overflow: ellipsis;
}
}
.mat-mdc-standard-chip {
.mdc-evolution-chip__cell--primary, .mdc-evolution-chip__action--primary, .mdc-evolution-chip__text-label {
overflow: hidden;
.arguments-table:not(.arguments-table-with-error) {
.mdc-data-table__row:last-child .mat-mdc-cell {
border-bottom: none;
}
}
.arguments-table {
.mat-mdc-header-row.mat-row-select .mat-mdc-header-cell.entity-type-header {
padding: 0 28px 0 0;
}
}
}

125
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts

@ -15,20 +15,22 @@
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
DestroyRef,
forwardRef,
Input,
OnChanges,
Renderer2,
SimpleChanges,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
@ -48,8 +50,11 @@ import { TbPopoverService } from '@shared/components/popover.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { getEntityDetailsPageURL, isDefined, isDefinedAndNotNull } from '@core/utils';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
import { EntityService } from '@core/http/entity.service';
import { MatSort } from '@angular/material/sort';
@Component({
selector: 'tb-calculated-field-arguments-table',
@ -68,19 +73,23 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
}
],
})
export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator, OnChanges {
export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator, OnChanges, AfterViewInit {
@Input() entityId: EntityId;
@Input() tenantId: string;
@Input() entityName: string;
@Input() calculatedFieldType: CalculatedFieldType;
@ViewChild(MatSort, { static: true }) sort: MatSort;
errorText = '';
argumentsFormArray = this.fb.array<AbstractControl>([]);
entityNameMap = new Map<string, string>();
sortOrder = { direction: 'asc', property: '' };
dataSource = new CalculatedFieldArgumentDatasource();
readonly entityTypeTranslations = entityTypeTranslations;
readonly ArgumentTypeTranslations = ArgumentTypeTranslations;
readonly EntityType = EntityType;
readonly ArgumentEntityType = ArgumentEntityType;
readonly ArgumentType = ArgumentType;
readonly CalculatedFieldType = CalculatedFieldType;
@ -93,10 +102,14 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
private popoverService: TbPopoverService,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private renderer: Renderer2
private renderer: Renderer2,
private entityService: EntityService,
private destroyRef: DestroyRef,
) {
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
this.propagateChange(this.getArgumentsObject());
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => {
this.updateEntityNameMap(value);
this.updateDataSource(value);
this.propagateChange(this.getArgumentsObject(value));
});
}
@ -107,6 +120,14 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}
}
ngAfterViewInit(): void {
this.sort.sortChange.asObservable().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.sortOrder.property = this.sort.active;
this.sortOrder.direction = this.sort.direction;
this.updateDataSource(this.argumentsFormArray.value);
});
}
registerOnChange(fn: (argumentsObj: Record<string, CalculatedFieldArgument>) => void): void {
this.propagateChange = fn;
}
@ -118,12 +139,13 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
return this.errorText ? { argumentsFormArray: false } : null;
}
onDelete(index: number): void {
onDelete($event: Event, index: number): void {
$event.stopPropagation();
this.argumentsFormArray.removeAt(index);
this.argumentsFormArray.markAsDirty();
}
manageArgument($event: Event, matButton: MatButton, index?: number): void {
manageArgument($event: Event, matButton: MatButton, argument = {} as CalculatedFieldArgumentValue, index?: number): void {
$event?.stopPropagation();
if (this.popoverComponent && !this.popoverComponent.tbHidden) {
this.popoverComponent.hide();
@ -132,16 +154,15 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const argumentObj = this.argumentsFormArray.at(index)?.getRawValue() ?? {};
const ctx = {
index,
argument: argumentObj,
argument,
entityId: this.entityId,
calculatedFieldType: this.calculatedFieldType,
buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
tenantId: this.tenantId,
entityName: this.entityName,
usedArgumentNames: this.argumentsFormArray.getRawValue().map(({ argumentName }) => argumentName).filter(name => name !== argumentObj.argumentName),
usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName),
};
this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, CalculatedFieldArgumentPanelComponent, isDefined(index) ? 'left' : 'right', false, null,
@ -150,7 +171,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
{}, {}, true);
this.popoverComponent.tbComponentRef.instance.argumentsDataApplied.subscribe(({ value, index }) => {
this.popoverComponent.hide();
const formGroup = this.getArgumentFormGroup(value);
const formGroup = this.fb.group(value);
if (isDefinedAndNotNull(index)) {
this.argumentsFormArray.setControl(index, formGroup);
} else {
@ -162,9 +183,14 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}
}
private updateDataSource(value: CalculatedFieldArgumentValue[]): void {
const sortedValue = this.sortData(value);
this.dataSource.loadData(sortedValue);
}
private updateErrorText(): void {
if (this.calculatedFieldType === CalculatedFieldType.SIMPLE
&& this.argumentsFormArray.controls.some(control => control.get('refEntityKey').get('type').value === ArgumentType.Rolling)) {
&& this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) {
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
} else if (!this.argumentsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.arguments-empty';
@ -173,9 +199,9 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}
}
private getArgumentsObject(): Record<string, CalculatedFieldArgument> {
return this.argumentsFormArray.getRawValue().reduce((acc, rawValue) => {
const { argumentName, ...argument } = rawValue as CalculatedFieldArgumentValue;
private getArgumentsObject(value: CalculatedFieldArgumentValue[]): Record<string, CalculatedFieldArgument> {
return value.reduce((acc, argumentValue) => {
const { argumentName, ...argument } = argumentValue as CalculatedFieldArgumentValue;
acc[argumentName] = argument;
return acc;
}, {} as Record<string, CalculatedFieldArgument>);
@ -186,31 +212,62 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
this.populateArgumentsFormArray(argumentsObj)
}
getEntityDetailsPageURL(id: string, type: EntityType): string {
return getEntityDetailsPageURL(id, type);
}
private populateArgumentsFormArray(argumentsObj: Record<string, CalculatedFieldArgument>): void {
Object.keys(argumentsObj).forEach(key => {
const value: CalculatedFieldArgumentValue = {
...argumentsObj[key],
argumentName: key
};
this.argumentsFormArray.push(this.getArgumentFormGroup(value), {emitEvent: false});
this.argumentsFormArray.push(this.fb.group(value), { emitEvent: false });
});
this.argumentsFormArray.updateValueAndValidity();
}
private updateEntityNameMap(value: CalculatedFieldArgumentValue[]): void {
value.forEach(({ refEntityId = {}}) => {
if (refEntityId.id && !this.entityNameMap.has(refEntityId.id)) {
const { id, entityType } = refEntityId as EntityId;
this.entityService.getEntity(entityType as EntityType, id, { ignoreLoading: true, ignoreErrors: true })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(entity => this.entityNameMap.set(id, entity.name));
}
});
}
private getSortValue(argument: CalculatedFieldArgumentValue, column: string): string {
switch (column) {
case 'entityType':
if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) {
return 'calculated-fields.argument-current-tenant';
} else if (argument.refEntityId?.id) {
return entityTypeTranslations.get((argument.refEntityId)?.entityType as unknown as EntityType).type;
} else {
return 'calculated-fields.argument-current';
}
case 'type':
return ArgumentTypeTranslations.get(argument.refEntityKey.type);
case 'key':
return argument.refEntityKey.key;
default:
return argument.argumentName;
}
}
private sortData(data: CalculatedFieldArgumentValue[]): CalculatedFieldArgumentValue[] {
return data.sort((a, b) => {
const valA = this.getSortValue(a, this.sortOrder.property) ?? '';
const valB = this.getSortValue(b, this.sortOrder.property) ?? '';
return (this.sortOrder.direction === 'asc' ? 1 : -1) * valA.localeCompare(valB);
});
}
}
private getArgumentFormGroup(value: CalculatedFieldArgumentValue): FormGroup {
return this.fb.group({
...value,
argumentName: [{ value: value.argumentName, disabled: true }],
...(value.refEntityId ? {
refEntityId: this.fb.group({
entityType: [{ value: value.refEntityId.entityType, disabled: true }],
id: [{ value: value.refEntityId.id , disabled: true }],
}),
} : {}),
refEntityKey: this.fb.group({
...value.refEntityKey,
type: [{ value: value.refEntityKey.type, disabled: true }],
key: [{ value: value.refEntityKey.key, disabled: true }],
}),
})
class CalculatedFieldArgumentDatasource extends TbTableDatasource<CalculatedFieldArgumentValue> {
constructor() {
super();
}
}

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -143,10 +143,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}
private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value ?? {};
const { configuration = {}, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration;
const updatedConfig = { ...restConfig , ['expression'+type]: expression };
this.fieldFormGroup.patchValue({ configuration: updatedConfig, type, ...value }, {emitEvent: false});
this.fieldFormGroup.patchValue({ configuration: updatedConfig, type, debugSettings, ...value }, {emitEvent: false});
}
private observeTypeChanges(): void {

101
ui-ngx/src/app/shared/components/js-func.component.ts

@ -37,7 +37,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, guid, isUndefined, isUndefinedOrNull } from '@app/core/utils';
import { deepClone, guid, isEqual, isObject, isUndefined, isUndefinedOrNull } from '@app/core/utils';
import { TranslateService } from '@ngx-translate/core';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
@ -179,26 +179,12 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal
private http: HttpClient) {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.functionArgs) {
this.updateFunctionArgsString();
this.updateFunctionLabel();
}
if (changes.editorCompleter?.previousValue) {
this.updateCompleters();
}
if (changes.highlightRules?.previousValue) {
this.updateHighlightRules();
}
}
ngOnInit(): void {
if (this.functionTitle || this.label) {
this.hideBrackets = true;
}
if (!this.resultType || this.resultType.length === 0) {
this.resultType = 'nocheck';
}
this.updateFunctionArgsString()
this.updateFunctionLabel();
const editorElement = this.javascriptEditorElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/javascript',
@ -262,6 +248,16 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal
);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const { firstChange, currentValue, previousValue } = changes[propName];
const isChanged = isObject(currentValue) ? !isEqual(currentValue, previousValue) : currentValue !== previousValue;
if (!firstChange && isChanged) {
this.updateByChangesPropName(propName);
}
}
}
ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
@ -271,24 +267,6 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal
}
}
private updateHighlightRules(): void {
// @ts-ignore
if (!!this.highlightRules && !!this.jsEditor.session.$mode) {
// @ts-ignore
const newMode = new this.jsEditor.session.$mode.constructor();
newMode.$highlightRules = new newMode.HighlightRules();
for(const group in this.highlightRules) {
if(!!newMode.$highlightRules.$rules[group]) {
newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]);
} else {
newMode.$highlightRules.$rules[group] = this.highlightRules[group];
}
}
// @ts-ignore
this.jsEditor.session.$onChangeMode(newMode);
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
@ -340,6 +318,9 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal
}
private updateFunctionLabel(): void {
if (this.functionTitle || this.label) {
this.hideBrackets = true;
}
if (this.functionTitle) {
this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`;
} else if (this.label) {
@ -351,6 +332,10 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal
this.cd.markForCheck();
}
private updatedScriptLanguage() {
this.jsEditor.session.setMode(`ace/mode/${ScriptLanguage.TBEL === this.scriptLanguage ? 'tbel' : 'javascript'}`);
}
validateOnSubmit(): Observable<void> {
if (!this.disabled) {
this.cleanupJsErrors();
@ -561,6 +546,52 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal
}
}
private updateByChangesPropName(propName: string): void {
switch (propName) {
case 'functionArgs':
this.updateFunctionArgsString()
this.updateFunctionLabel();
this.updateJsWorkerGlobals();
break;
case 'label':
case 'functionTitle':
case 'functionName':
this.updateFunctionLabel();
break;
case 'scriptLanguage':
this.updatedScriptLanguage();
break;
case 'disableUndefinedCheck':
case 'globalVariables':
this.updateJsWorkerGlobals();
break;
case 'editorCompleter':
this.updateCompleters();
break;
case 'highlightRules':
this.updateHighlightRules();
break;
}
}
private updateHighlightRules(): void {
// @ts-ignore
if (!!this.highlightRules && !!this.jsEditor.session.$mode) {
// @ts-ignore
const newMode = new this.jsEditor.session.$mode.constructor();
newMode.$highlightRules = new newMode.HighlightRules();
for(const group in this.highlightRules) {
if(!!newMode.$highlightRules.$rules[group]) {
newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]);
} else {
newMode.$highlightRules.$rules[group] = this.highlightRules[group];
}
}
// @ts-ignore
this.jsEditor.session.$onChangeMode(newMode);
}
}
private updateJsWorkerGlobals() {
// @ts-ignore
if (!!this.jsEditor.session.$worker) {

5
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -16,7 +16,7 @@
import {
AdditionalDebugActionConfig,
EntityDebugSettings,
HasEntityDebugSettings,
HasTenantId,
HasVersion
} from '@shared/models/entity.models';
@ -34,8 +34,7 @@ import {
endGroupHighlightRule
} from '@shared/models/ace/ace.models';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
debugSettings?: EntityDebugSettings;
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
configuration: CalculatedFieldConfiguration;
type: CalculatedFieldType;
entityId: EntityId;

Loading…
Cancel
Save