Browse Source

Merge with main branch

pull/12625/head
Andrii Shvaika 2 years ago
parent
commit
0d57d69964
  1. 91
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 4
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  3. 12
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldExecutionService.java
  5. 4
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldExecutionService.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/CalculatedFieldStateService.java
  8. 17
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  9. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  11. 82
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBStateService.java
  12. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  13. 6
      application/src/main/resources/thingsboard.yml
  14. 12
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  15. 13
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  16. 6
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java
  18. 4
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java
  19. 6
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  20. 22
      common/proto/src/main/proto/queue.proto
  21. 2
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  22. 14
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
  23. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/event/CalculatedFieldDebugEventRepository.java
  24. 4
      dao/src/main/resources/sql/schema-entities.sql
  25. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  26. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
  27. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  28. 8
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss
  29. 16
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts
  30. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  31. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  32. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html
  33. 30
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts
  34. 4
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html
  35. 4
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html
  36. 4
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  37. 22
      ui-ngx/src/app/shared/models/calculated-field.models.ts

91
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -98,6 +98,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
@ -137,6 +138,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
@ -179,6 +181,8 @@ public class ActorSystemContext {
private final ConcurrentMap<TenantId, DebugTbRateLimits> debugPerTenantLimits = new ConcurrentHashMap<>(); private final ConcurrentMap<TenantId, DebugTbRateLimits> debugPerTenantLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, TbRateLimits> cfDebugPerTenantLimits = new ConcurrentHashMap<>();
public ConcurrentMap<TenantId, DebugTbRateLimits> getDebugPerTenantLimits() { public ConcurrentMap<TenantId, DebugTbRateLimits> getDebugPerTenantLimits() {
return debugPerTenantLimits; return debugPerTenantLimits;
} }
@ -441,6 +445,11 @@ public class ActorSystemContext {
@Getter @Getter
private TbCoreToTransportService tbCoreToTransportService; private TbCoreToTransportService tbCoreToTransportService;
@Lazy
@Autowired(required = false)
@Getter
private ApiLimitService apiLimitService;
/** /**
* The following Service will be null if we operate in tb-core mode * The following Service will be null if we operate in tb-core mode
*/ */
@ -625,6 +634,14 @@ public class ActorSystemContext {
@Getter @Getter
private String deviceStateNodeRateLimitConfig; private String deviceStateNodeRateLimitConfig;
@Value("${actors.calculated_fields.debug_mode_rate_limits_per_tenant.enabled:true}")
@Getter
private boolean cfDebugPerTenantEnabled;
@Value("${actors.calculated_fields.debug_mode_rate_limits_per_tenant.configuration:50000:3600}")
@Getter
private String cfDebugPerTenantLimitsConfiguration;
@Getter @Getter
@Setter @Setter
private TbActorSystem actorSystem; private TbActorSystem actorSystem;
@ -753,37 +770,6 @@ public class ActorSystemContext {
} }
} }
public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map<String, ArgumentEntry> arguments, UUID tbMsgId, TbMsgType tbMsgType, String result, Throwable error) {
try {
CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder()
.tenantId(tenantId)
.entityId(entityId.getId())
.serviceId(getServiceId())
.calculatedFieldId(calculatedFieldId)
.eventEntity(entityId);
if (tbMsgId != null) {
eventBuilder.msgId(tbMsgId);
}
if (tbMsgType != null) {
eventBuilder.msgType(tbMsgType.name());
}
if (arguments != null) {
eventBuilder.arguments(JacksonUtil.toString(arguments));
}
if (result != null) {
eventBuilder.result(result);
}
if (error != null) {
eventBuilder.error(toString(error));
}
ListenableFuture<Void> future = eventService.saveAsync(eventBuilder.build());
Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} catch (IllegalArgumentException ex) {
log.warn("Failed to persist calculated field debug message", ex);
}
}
private boolean checkLimits(TenantId tenantId, TbMsg tbMsg, Throwable error) { private boolean checkLimits(TenantId tenantId, TbMsg tbMsg, Throwable error) {
if (debugPerTenantEnabled) { if (debugPerTenantEnabled) {
DebugTbRateLimits debugTbRateLimits = debugPerTenantLimits.computeIfAbsent(tenantId, id -> DebugTbRateLimits debugTbRateLimits = debugPerTenantLimits.computeIfAbsent(tenantId, id ->
@ -817,6 +803,49 @@ public class ActorSystemContext {
Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} }
public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map<String, ArgumentEntry> arguments, UUID tbMsgId, TbMsgType tbMsgType, String result, Throwable error) {
if (cfDebugPerTenantEnabled) {
TbRateLimits rateLimits = cfDebugPerTenantLimits.computeIfAbsent(tenantId, id -> new TbRateLimits(cfDebugPerTenantLimitsConfiguration));
if (rateLimits.tryConsume()) {
try {
CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder()
.tenantId(tenantId)
.entityId(calculatedFieldId.getId())
.serviceId(getServiceId())
.calculatedFieldId(calculatedFieldId)
.eventEntity(entityId);
if (tbMsgId != null) {
eventBuilder.msgId(tbMsgId);
}
if (tbMsgType != null) {
eventBuilder.msgType(tbMsgType.name());
}
if (arguments != null) {
eventBuilder.arguments(JacksonUtil.toString(
arguments.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getValue()))
));
}
if (result != null) {
eventBuilder.result(result);
}
if (error != null) {
eventBuilder.error(toString(error));
}
ListenableFuture<Void> future = eventService.saveAsync(eventBuilder.build());
Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} catch (IllegalArgumentException ex) {
log.warn("Failed to persist calculated field debug message", ex);
}
if (log.isTraceEnabled()) {
log.trace("[{}] Tenant level debug mode rate limit detected: {}", tenantId, calculatedFieldId);
}
}
}
}
public static Exception toException(Throwable error) { public static Exception toException(Throwable error) {
return Exception.class.isInstance(error) ? (Exception) error : new Exception(error); return Exception.class.isInstance(error) ? (Exception) error : new Exception(error);
} }

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

@ -200,7 +200,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
@SneakyThrows @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) {
if (state.isReady()) { if (state.isReady() && ctx.isInitialized()) {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS); CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS);
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
@ -209,7 +209,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else { } else {
callback.onSuccess(); // State was updated but no calculation performed; callback.onSuccess(); // State was updated but no calculation performed;
} }
cfService.pushStateToStorage(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), state, callback); cfService.pushStateToStorage(ctx, new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), state, callback);
} }
private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, List<TsKvProto> data) { private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, List<TsKvProto> data) {

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

@ -92,7 +92,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void onFieldInitMsg(CalculatedFieldInitMsg msg) { public void onFieldInitMsg(CalculatedFieldInitMsg msg) {
var cf = msg.getCf(); var cf = msg.getCf();
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService()); var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService());
try { try {
cfCtx.init(); cfCtx.init();
} catch (Exception e) { } catch (Exception e) {
@ -116,7 +116,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) { public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) {
if (calculatedFields.containsKey(msg.getId().cfId())) { var cfId = msg.getId().cfId();
var calculatedField = calculatedFields.get(cfId);
if (calculatedField != null) {
msg.getState().setRequiredArguments(calculatedField.getArgNames());
getOrCreateActor(msg.getId().entityId()).tell(msg); getOrCreateActor(msg.getId().entityId()).tell(msg);
} else { } else {
cfExecService.deleteStateFromStorage(msg.getId(), msg.getCallback()); cfExecService.deleteStateFromStorage(msg.getId(), msg.getCallback());
@ -217,7 +221,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.warn("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); log.warn("[{}] Failed to lookup CF by id [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
} else { } else {
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService()); var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService());
try { try {
cfCtx.init(); cfCtx.init();
} catch (Exception e) { } catch (Exception e) {
@ -245,7 +249,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.warn("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); log.warn("[{}] Failed to lookup CF by id [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
} else { } else {
var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService()); var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService());
calculatedFields.put(newCf.getId(), newCfCtx); calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getId()); List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getId());
List<CalculatedFieldCtx> newCfList = new ArrayList<>(oldCfList.size()); List<CalculatedFieldCtx> newCfList = new ArrayList<>(oldCfList.size());

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

@ -43,7 +43,7 @@ public interface CalculatedFieldExecutionService {
void pushRequestToQueue(AttributesSaveRequest request, List<Long> result, FutureCallback<Void> callback); void pushRequestToQueue(AttributesSaveRequest request, List<Long> result, FutureCallback<Void> callback);
void pushStateToStorage(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback); void pushStateToStorage(CalculatedFieldCtx ctx, CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback);
ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId); ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId);

4
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.Collections; import java.util.Collections;
@ -55,6 +56,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
private final CalculatedFieldService calculatedFieldService; private final CalculatedFieldService calculatedFieldService;
private final TbelInvokeService tbelInvokeService; private final TbelInvokeService tbelInvokeService;
private final ActorSystemContext actorSystemContext; private final ActorSystemContext actorSystemContext;
private final ApiLimitService apiLimitService;
private final ConcurrentMap<CalculatedFieldId, CalculatedField> calculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap<CalculatedFieldId, CalculatedField> calculatedFields = new ConcurrentHashMap<>();
private final ConcurrentMap<EntityId, List<CalculatedField>> entityIdCalculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap<EntityId, List<CalculatedField>> entityIdCalculatedFields = new ConcurrentHashMap<>();
@ -109,7 +111,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
if (ctx == null) { if (ctx == null) {
CalculatedField calculatedField = getCalculatedField(calculatedFieldId); CalculatedField calculatedField = getCalculatedField(calculatedFieldId);
if (calculatedField != null) { if (calculatedField != null) {
ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService); ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService);
calculatedFieldsCtx.put(calculatedFieldId, ctx); calculatedFieldsCtx.put(calculatedFieldId, ctx);
log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx);
} }

4
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldExecutionService.java

@ -265,8 +265,8 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
} }
@Override @Override
public void pushStateToStorage(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) { public void pushStateToStorage(CalculatedFieldCtx ctx, CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) {
stateService.persistState(stateId, state, callback); stateService.persistState(ctx, stateId, state, callback);
} }
@Override @Override

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/CalculatedFieldStateService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.cf.ctx; package org.thingsboard.server.service.cf.ctx;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import java.util.Map; import java.util.Map;
@ -24,7 +25,7 @@ public interface CalculatedFieldStateService {
Map<CalculatedFieldEntityCtxId, CalculatedFieldState> restoreStates(); Map<CalculatedFieldEntityCtxId, CalculatedFieldState> restoreStates();
void persistState(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback); void persistState(CalculatedFieldCtx ctx, CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback);
void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback); void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback);

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

@ -15,13 +15,16 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state; package org.thingsboard.server.service.cf.ctx.state;
import lombok.NoArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@NoArgsConstructor @Data
@AllArgsConstructor
public abstract class BaseCalculatedFieldState implements CalculatedFieldState { public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected List<String> requiredArguments; protected List<String> requiredArguments;
@ -32,14 +35,8 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
this.arguments = new HashMap<>(); this.arguments = new HashMap<>();
} }
@Override public BaseCalculatedFieldState() {
public Map<String, ArgumentEntry> getArguments() { this(new ArrayList<>(), new HashMap<>());
return arguments;
}
@Override
public List<String> getRequiredArguments() {
return requiredArguments;
} }
@Override @Override

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

@ -33,8 +33,10 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
@ -67,7 +69,10 @@ public class CalculatedFieldCtx {
private boolean initialized; private boolean initialized;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService) { private long maxDataPointsPerRollingArg;
private long maxStateSizeInKBytes;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) {
this.calculatedField = calculatedField; this.calculatedField = calculatedField;
this.cfId = calculatedField.getId(); this.cfId = calculatedField.getId();
@ -96,6 +101,9 @@ public class CalculatedFieldCtx {
this.output = configuration.getOutput(); this.output = configuration.getOutput();
this.expression = configuration.getExpression(); this.expression = configuration.getExpression();
this.tbelInvokeService = tbelInvokeService; this.tbelInvokeService = tbelInvokeService;
this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
this.maxStateSizeInKBytes = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes);
} }
public void init() { public void init() {

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

@ -43,6 +43,8 @@ public interface CalculatedFieldState {
List<String> getRequiredArguments(); List<String> getRequiredArguments();
void setRequiredArguments(List<String> requiredArguments);
boolean updateState(Map<String, ArgumentEntry> argumentValues); boolean updateState(Map<String, ArgumentEntry> argumentValues);
ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx); ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx);

82
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBStateService.java

@ -25,19 +25,19 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.RollingArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueProto; import org.thingsboard.server.gen.transport.TransportProtos.TsValueListProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto;
import org.thingsboard.server.service.cf.RocksDBService; import org.thingsboard.server.service.cf.RocksDBService;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldStateService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldStateService;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -59,8 +59,12 @@ public class RocksDBStateService implements CalculatedFieldStateService {
} }
@Override @Override
public void persistState(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) { public void persistState(CalculatedFieldCtx ctx, CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) {
rocksDBService.put(toProto(stateId), toProto(stateId, state)); CalculatedFieldStateProto stateProto = toProto(stateId, state);
long maxStateSizeInKBytes = ctx.getMaxStateSizeInKBytes();
if (maxStateSizeInKBytes <= 0 || stateProto.getSerializedSize() <= ctx.getMaxStateSizeInKBytes()) {
rocksDBService.put(toProto(stateId), toProto(stateId, state));
}
callback.onSuccess(); callback.onSuccess();
} }
@ -92,8 +96,7 @@ public class RocksDBStateService implements CalculatedFieldStateService {
private CalculatedFieldStateProto toProto(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state) { private CalculatedFieldStateProto toProto(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state) {
CalculatedFieldStateProto.Builder builder = CalculatedFieldStateProto.newBuilder() CalculatedFieldStateProto.Builder builder = CalculatedFieldStateProto.newBuilder()
.setId(toProto(stateId)) .setId(toProto(stateId))
.setType(state.getType().name()) .setType(state.getType().name());
.addAllRequiredArguments(state.getRequiredArguments());
state.getArguments().forEach((argName, argEntry) -> { state.getArguments().forEach((argName, argEntry) -> {
if (argEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { if (argEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
@ -107,42 +110,21 @@ public class RocksDBStateService implements CalculatedFieldStateService {
} }
private SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) { private SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) {
SingleValueProto.Builder singleValueProtoBuilder = SingleValueProto.newBuilder() SingleValueArgumentProto.Builder builder = SingleValueArgumentProto.newBuilder()
.setTs(entry.getTs());
if (entry.getVersion() != null) {
singleValueProtoBuilder.setVersion(entry.getVersion());
}
KvEntry value = entry.getKvEntryValue();
if (value != null) {
singleValueProtoBuilder.setHasV(true)
.setValue(ProtoUtils.toKeyValueProto(value));
}
return SingleValueArgumentProto.newBuilder()
.setArgName(argName) .setArgName(argName)
.setValue(singleValueProtoBuilder.build()) .setValue(KvProtoUtil.toTsValueProto(entry.getTs(), entry.getKvEntryValue()));
.build();
}
private RollingArgumentProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) { Optional.ofNullable(entry.getVersion()).ifPresent(builder::setVersion);
RollingArgumentProto.Builder rollingArgumentProtoBuilder = RollingArgumentProto.newBuilder()
.setArgName(argName);
entry.getTsRecords().forEach((ts, value) -> { return builder.build();
SingleValueProto.Builder singleValueProtoBuilder = SingleValueProto.newBuilder() }
.setTs(ts);
if (value != null) { private TsValueListProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) {
singleValueProtoBuilder.setHasV(true) TsValueListProto.Builder builder = TsValueListProto.newBuilder().setKey(argName);
.setValue(ProtoUtils.toKeyValueProto(value));
}
rollingArgumentProtoBuilder.addValues(singleValueProtoBuilder.build()); entry.getTsRecords().forEach((ts, value) -> builder.addTsValue(KvProtoUtil.toTsValueProto(ts, value)));
});
return rollingArgumentProtoBuilder.build(); return builder.build();
} }
private CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { private CalculatedFieldState fromProto(CalculatedFieldStateProto proto) {
@ -153,8 +135,8 @@ public class RocksDBStateService implements CalculatedFieldStateService {
CalculatedFieldType type = CalculatedFieldType.valueOf(proto.getType()); CalculatedFieldType type = CalculatedFieldType.valueOf(proto.getType());
CalculatedFieldState state = switch (type) { CalculatedFieldState state = switch (type) {
case SIMPLE -> new SimpleCalculatedFieldState(proto.getRequiredArgumentsList()); case SIMPLE -> new SimpleCalculatedFieldState();
case SCRIPT -> new ScriptCalculatedFieldState(proto.getRequiredArgumentsList()); case SCRIPT -> new ScriptCalculatedFieldState();
}; };
proto.getSingleValueArgumentsList().forEach(argProto -> proto.getSingleValueArgumentsList().forEach(argProto ->
@ -162,27 +144,25 @@ public class RocksDBStateService implements CalculatedFieldStateService {
if (CalculatedFieldType.SCRIPT.equals(type)) { if (CalculatedFieldType.SCRIPT.equals(type)) {
proto.getRollingValueArgumentsList().forEach(argProto -> proto.getRollingValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getArgName(), fromRollingArgumentProto(argProto))); state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto)));
} }
return state; return state;
} }
private SingleValueArgumentEntry fromSingleValueArgumentProto(SingleValueArgumentProto proto) { private SingleValueArgumentEntry fromSingleValueArgumentProto(SingleValueArgumentProto proto) {
SingleValueProto valueProto = proto.getValue(); TsValueProto tsValueProto = proto.getValue();
BasicKvEntry value = valueProto.getHasV() ? ProtoUtils.fromProto(valueProto.getValue()) : null; long ts = tsValueProto.getTs();
BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto);
return new SingleValueArgumentEntry(valueProto.getTs(), value, valueProto.getVersion()); return new SingleValueArgumentEntry(ts, kvEntry, proto.getVersion());
} }
private TsRollingArgumentEntry fromRollingArgumentProto(RollingArgumentProto proto) { private TsRollingArgumentEntry fromRollingArgumentProto(TsValueListProto proto) {
TreeMap<Long, BasicKvEntry> tsRecords = new TreeMap<>(); TreeMap<Long, BasicKvEntry> tsRecords = new TreeMap<>();
proto.getTsValueList().forEach(tsValueProto -> {
proto.getValuesList().forEach(singleValueProto -> { BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getKey(), tsValueProto);
BasicKvEntry value = singleValueProto.getHasV() ? ProtoUtils.fromProto(singleValueProto.getValue()) : null; tsRecords.put(tsValueProto.getTs(), kvEntry);
tsRecords.put(singleValueProto.getTs(), value);
}); });
return new TsRollingArgumentEntry(tsRecords); return new TsRollingArgumentEntry(tsRecords);
} }

1
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -55,6 +55,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ)); put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ));
put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker);
} }
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

6
application/src/main/resources/thingsboard.yml

@ -510,6 +510,12 @@ actors:
js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}" js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}"
# Actors statistic persistence frequency in milliseconds # Actors statistic persistence frequency in milliseconds
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}" persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
calculated_fields:
debug_mode_rate_limits_per_tenant:
# Enable/Disable the rate limit of persisted debug events for all calculated fields per tenant
enabled: "${ACTORS_CF_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}"
# The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour
configuration: "${ACTORS_CF_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}"
debug: debug:
settings: settings:

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

@ -19,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.HashMap; import java.util.HashMap;
@ -45,6 +47,8 @@ import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@SpringBootTest(classes = DefaultTbelInvokeService.class) @SpringBootTest(classes = DefaultTbelInvokeService.class)
public class ScriptCalculatedFieldStateTest { public class ScriptCalculatedFieldStateTest {
@ -64,9 +68,13 @@ public class ScriptCalculatedFieldStateTest {
@Autowired @Autowired
private TbelInvokeService tbelInvokeService; private TbelInvokeService tbelInvokeService;
@MockBean
private ApiLimitService apiLimitService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService); when(apiLimitService.getLimit(any(), any())).thenReturn(1000L);
ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService);
ctx.init(); ctx.init();
state = new ScriptCalculatedFieldState(ctx.getArgNames()); state = new ScriptCalculatedFieldState(ctx.getArgNames());
} }
@ -219,7 +227,7 @@ public class ScriptCalculatedFieldStateTest {
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("temperature", ArgumentType.TS_ROLLING, null); ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("temperature", ArgumentType.TS_ROLLING, null);
argument1.setRefEntityKey(refEntityKey1); argument1.setRefEntityKey(refEntityKey1);
argument1.setLimit(5); argument1.setLimit(5);
argument1.setTimeWindow(30000); argument1.setTimeWindow(30000L);
Argument argument2 = new Argument(); Argument argument2 = new Argument();
ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null); ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null);

13
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -32,6 +35,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.HashMap; import java.util.HashMap;
@ -41,7 +45,10 @@ import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class SimpleCalculatedFieldStateTest { public class SimpleCalculatedFieldStateTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382")); private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382"));
@ -55,9 +62,13 @@ public class SimpleCalculatedFieldStateTest {
private SimpleCalculatedFieldState state; private SimpleCalculatedFieldState state;
private CalculatedFieldCtx ctx; private CalculatedFieldCtx ctx;
@Mock
private ApiLimitService apiLimitService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
ctx = new CalculatedFieldCtx(getCalculatedField(), null); when(apiLimitService.getLimit(any(), any())).thenReturn(1000L);
ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService);
ctx.init(); ctx.init();
state = new SimpleCalculatedFieldState(ctx.getArgNames()); state = new SimpleCalculatedFieldState(ctx.getArgNames());
} }

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

@ -15,11 +15,13 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data; import lombok.Data;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@Data @Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Argument { public class Argument {
@Nullable @Nullable
@ -27,7 +29,7 @@ public class Argument {
private ReferencedEntityKey refEntityKey; private ReferencedEntityKey refEntityKey;
private String defaultValue; private String defaultValue;
private int limit; private Integer limit;
private long timeWindow; private Long timeWindow;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java

@ -15,10 +15,12 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
@Data @Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Output { public class Output {
private String name; private String name;

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

@ -15,18 +15,18 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ReferencedEntityKey { public class ReferencedEntityKey {
private String key; private String key;
private ArgumentType type; private ArgumentType type;
private AttributeScope scope; private AttributeScope scope;
} }

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

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

22
common/proto/src/main/proto/queue.proto

@ -790,7 +790,7 @@ message CalculatedFieldTelemetryMsgProto {
message CalculatedFieldLinkedTelemetryMsgProto { message CalculatedFieldLinkedTelemetryMsgProto {
CalculatedFieldTelemetryMsgProto msg = 1; CalculatedFieldTelemetryMsgProto msg = 1;
repeated CalculatedFieldEntityCtxIdProto links = 2; repeated CalculatedFieldEntityCtxIdProto links = 2;
} }
message CalculatedFieldEntityCtxIdProto { message CalculatedFieldEntityCtxIdProto {
@ -808,30 +808,18 @@ message CalculatedFieldIdProto {
int64 calculatedFieldIdLSB = 2; int64 calculatedFieldIdLSB = 2;
} }
message SingleValueProto {
int64 ts = 1;
int64 version = 2;
bool has_v = 4;
KeyValueProto value = 5;
}
message SingleValueArgumentProto { message SingleValueArgumentProto {
string argName = 1; string argName = 1;
SingleValueProto value = 2; TsValueProto value = 2;
} int64 version = 3;
message RollingArgumentProto {
string argName = 1;
repeated SingleValueProto values = 2;
} }
message CalculatedFieldStateProto { message CalculatedFieldStateProto {
CalculatedFieldEntityCtxIdProto id = 1; CalculatedFieldEntityCtxIdProto id = 1;
// int32 version = 2; // int32 version = 2;
string type = 3; string type = 3;
repeated string requiredArguments = 4; repeated SingleValueArgumentProto singleValueArguments = 4;
repeated SingleValueArgumentProto singleValueArguments = 5; repeated TsValueListProto rollingValueArguments = 5;
repeated RollingArgumentProto rollingValueArguments = 6;
} }
//Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level.

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

@ -62,9 +62,9 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
try { try {
TenantId tenantId = calculatedField.getTenantId(); TenantId tenantId = calculatedField.getTenantId();
log.trace("Executing save calculated field, [{}]", calculatedField); log.trace("Executing save calculated field, [{}]", calculatedField);
updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis());
CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField);
createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField);
updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis());
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId())
.entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build());
return savedCalculatedField; return savedCalculatedField;

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

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

2
dao/src/main/java/org/thingsboard/server/dao/sql/event/CalculatedFieldDebugEventRepository.java

@ -35,7 +35,7 @@ public interface CalculatedFieldDebugEventRepository extends EventRepository<Cal
List<CalculatedFieldDebugEventEntity> findLatestEvents(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId, @Param("limit") int limit); List<CalculatedFieldDebugEventEntity> findLatestEvents(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId, @Param("limit") int limit);
@Override @Override
@Query("SELECT e FROM RuleNodeDebugEventEntity e WHERE " + @Query("SELECT e FROM CalculatedFieldDebugEventEntity e WHERE " +
"e.tenantId = :tenantId " + "e.tenantId = :tenantId " +
"AND e.entityId = :entityId " + "AND e.entityId = :entityId " +
"AND (:startTime IS NULL OR e.ts >= :startTime) " + "AND (:startTime IS NULL OR e.ts >= :startTime) " +

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

@ -955,10 +955,10 @@ CREATE TABLE IF NOT EXISTS cf_debug_event (
id uuid NOT NULL, id uuid NOT NULL,
tenant_id uuid NOT NULL , tenant_id uuid NOT NULL ,
ts bigint NOT NULL, ts bigint NOT NULL,
entity_id uuid NOT NULL, entity_id uuid NOT NULL, -- calculated field id
service_id varchar, service_id varchar,
cf_id uuid NOT NULL, cf_id uuid NOT NULL,
e_entity_id uuid, e_entity_id uuid, -- target entity id
e_entity_type varchar, e_entity_type varchar,
e_msg_id uuid, e_msg_id uuid,
e_msg_type varchar, e_msg_type varchar,

4
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -54,7 +54,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private durationLeft: DurationLeftPipe, private durationLeft: DurationLeftPipe,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private renderer: Renderer2 private renderer: Renderer2,
public entityName: string
) { ) {
super(); super();
this.tableTitle = this.translate.instant('entity.type-calculated-fields'); this.tableTitle = this.translate.instant('entity.type-calculated-fields');
@ -159,6 +160,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
entityId: this.entityId, entityId: this.entityId,
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration, debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName,
} }
}) })
.afterClosed(); .afterClosed();

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

@ -47,6 +47,7 @@ export class CalculatedFieldsTableComponent {
active = input<boolean>(); active = input<boolean>();
entityId = input<EntityId>(); entityId = input<EntityId>();
entityName = input<string>();
calculatedFieldsTableConfig: CalculatedFieldsTableConfig; calculatedFieldsTableConfig: CalculatedFieldsTableConfig;
@ -71,7 +72,8 @@ export class CalculatedFieldsTableComponent {
this.durationLeft, this.durationLeft,
this.popoverService, this.popoverService,
this.destroyRef, this.destroyRef,
this.renderer this.renderer,
this.entityName()
); );
this.cd.markForCheck(); this.cd.markForCheck();
} }

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

@ -80,14 +80,23 @@
</mat-chip> </mat-chip>
</mat-chip-listbox> </mat-chip-listbox>
</ng-container> </ng-container>
<div class="flex opacity-55"> <div class="tb-form-table-row-cell-buttons flex">
<button type="button" <button type="button"
mat-icon-button mat-icon-button
#button #button
(click)="manageArgument($event, button, $index)" (click)="manageArgument($event, button, $index)"
[matTooltip]="'action.edit' | translate" [matTooltip]="'action.edit' | translate"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>edit</mat-icon> <mat-icon
[matBadgeHidden]="!(argumentsFormArray.dirty
&& group.get('refEntityKey').get('type').value === ArgumentType.Rolling
&& calculatedFieldType() === CalculatedFieldType.SIMPLE)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button> </button>
<button type="button" <button type="button"
mat-icon-button mat-icon-button

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

@ -13,6 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host {
.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 { :host ::ng-deep {
.tb-inline-field { .tb-inline-field {
a { a {

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

@ -21,7 +21,9 @@ import {
forwardRef, forwardRef,
input, input,
Input, Input,
OnChanges,
Renderer2, Renderer2,
SimpleChanges,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { import {
@ -70,10 +72,11 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
} }
], ],
}) })
export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator { export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator, OnChanges {
@Input() entityId: EntityId; @Input() entityId: EntityId;
@Input() tenantId: string; @Input() tenantId: string;
@Input() entityName: string;
calculatedFieldType = input<CalculatedFieldType>() calculatedFieldType = input<CalculatedFieldType>()
@ -84,6 +87,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
readonly ArgumentTypeTranslations = ArgumentTypeTranslations; readonly ArgumentTypeTranslations = ArgumentTypeTranslations;
readonly EntityType = EntityType; readonly EntityType = EntityType;
readonly ArgumentEntityType = ArgumentEntityType; readonly ArgumentEntityType = ArgumentEntityType;
readonly ArgumentType = ArgumentType;
readonly CalculatedFieldType = CalculatedFieldType;
private popoverComponent: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>; private popoverComponent: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>;
private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {}; private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {};
@ -105,6 +110,13 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}); });
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes.calculatedFieldType?.previousValue
&& changes.calculatedFieldType.currentValue !== changes.calculatedFieldType.previousValue) {
this.argumentsFormArray.markAsDirty();
}
}
registerOnChange(fn: (argumentsObj: Record<string, CalculatedFieldArgument>) => void): void { registerOnChange(fn: (argumentsObj: Record<string, CalculatedFieldArgument>) => void): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -137,6 +149,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
calculatedFieldType: this.calculatedFieldType(), calculatedFieldType: this.calculatedFieldType(),
buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add', buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName,
}; };
this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer, this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null, this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null,
@ -202,6 +215,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}), }),
} : {}), } : {}),
refEntityKey: this.fb.group({ refEntityKey: this.fb.group({
...value.refEntityKey,
type: [{ value: value.refEntityKey.type, disabled: true }], type: [{ value: value.refEntityKey.type, disabled: true }],
key: [{ value: value.refEntityKey.key, disabled: true }], key: [{ value: value.refEntityKey.key, disabled: true }],
}), }),

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -69,6 +69,7 @@
formControlName="arguments" formControlName="arguments"
[entityId]="data.entityId" [entityId]="data.entityId"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[entityName]="data.entityName"
[calculatedFieldType]="fieldFormGroup.get('type').value" [calculatedFieldType]="fieldFormGroup.get('type').value"
/> />
</div> </div>
@ -114,7 +115,7 @@
</mat-form-field> </mat-form-field>
@if (outputFormGroup.get('type').value === OutputType.Attribute) { @if (outputFormGroup.get('type').value === OutputType.Attribute) {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label> <mat-label>{{ 'calculated-fields.attribute-scope' | translate }}</mat-label>
<mat-select formControlName="scope" class="w-full"> <mat-select formControlName="scope" class="w-full">
<mat-option [value]="AttributeScope.SERVER_SCOPE"> <mat-option [value]="AttributeScope.SERVER_SCOPE">
{{ 'calculated-fields.server-attributes' | translate }} {{ 'calculated-fields.server-attributes' | translate }}

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

@ -59,10 +59,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}), }),
}); });
functionArgs$ = this.configFormGroup.valueChanges functionArgs$ = this.configFormGroup.get('arguments').valueChanges
.pipe( .pipe(
startWith(this.data.value?.configuration ?? {}), startWith(this.data.value?.configuration?.arguments ?? {}),
map(configuration => Object.keys(configuration.arguments)) map(argumentsObj => Object.keys(argumentsObj))
); );
readonly OutputTypeTranslations = OutputTypeTranslations; readonly OutputTypeTranslations = OutputTypeTranslations;

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html

@ -96,7 +96,7 @@
} }
</mat-form-field> </mat-form-field>
</div> </div>
@if (entityFilter.singleEntity.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) { @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) {
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.timeseries-key' | translate }}</div> <div class="fixed-title-width tb-required">{{ 'calculated-fields.timeseries-key' | translate }}</div>

30
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts

@ -25,7 +25,8 @@ import {
ArgumentType, ArgumentType,
ArgumentTypeTranslations, ArgumentTypeTranslations,
CalculatedFieldArgumentValue, CalculatedFieldArgumentValue,
CalculatedFieldType CalculatedFieldType,
getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
@ -49,6 +50,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
@Input() argument: CalculatedFieldArgumentValue; @Input() argument: CalculatedFieldArgumentValue;
@Input() entityId: EntityId; @Input() entityId: EntityId;
@Input() tenantId: string; @Input() tenantId: string;
@Input() entityName: string;
@Input() calculatedFieldType: CalculatedFieldType; @Input() calculatedFieldType: CalculatedFieldType;
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
@ -83,6 +85,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
readonly ArgumentEntityType = ArgumentEntityType; readonly ArgumentEntityType = ArgumentEntityType;
readonly ArgumentEntityTypeParamsMap = ArgumentEntityTypeParamsMap; readonly ArgumentEntityTypeParamsMap = ArgumentEntityTypeParamsMap;
private currentEntityFilter: EntityFilter;
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@ -107,6 +111,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); this.argumentFormGroup.patchValue(this.argument, {emitEvent: false});
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId);
this.updateEntityFilter(this.argument.refEntityId?.entityType, true); this.updateEntityFilter(this.argument.refEntityId?.entityType, true);
this.toggleByEntityKeyType(this.argument.refEntityKey?.type); this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
this.setInitialEntityKeyType(); this.setInitialEntityKeyType();
@ -138,27 +143,30 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
} }
private updateEntityFilter(entityType: ArgumentEntityType = ArgumentEntityType.Current, onInit = false): void { private updateEntityFilter(entityType: ArgumentEntityType = ArgumentEntityType.Current, onInit = false): void {
let entityId: EntityId; let entityFilter: EntityFilter;
switch (entityType) { switch (entityType) {
case ArgumentEntityType.Current: case ArgumentEntityType.Current:
entityId = this.entityId entityFilter = this.currentEntityFilter;
break; break;
case ArgumentEntityType.Tenant: case ArgumentEntityType.Tenant:
entityId = { entityFilter = {
id: this.tenantId, type: AliasFilterType.singleEntity,
entityType: EntityType.TENANT singleEntity: {
id: this.tenantId,
entityType: EntityType.TENANT
},
}; };
break; break;
default: default:
entityId = this.argumentFormGroup.get('refEntityId').value as unknown as EntityId; entityFilter = {
type: AliasFilterType.singleEntity,
singleEntity: this.argumentFormGroup.get('refEntityId').value as unknown as EntityId,
};
} }
if (!onInit) { if (!onInit) {
this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); this.argumentFormGroup.get('refEntityKey').get('key').setValue('');
} }
this.entityFilter = { this.entityFilter = entityFilter;
type: AliasFilterType.singleEntity,
singleEntity: entityId,
};
this.cd.markForCheck(); this.cd.markForCheck();
} }

4
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html

@ -15,6 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [entityName]="entity.name"/>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit" #auditLogsTab="matTab" <mat-tab *ngIf="entity && !isEdit" #auditLogsTab="matTab"
label="{{ 'audit-log.audit-logs' | translate }}"> label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table> <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>

4
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html

@ -32,6 +32,10 @@
[entityName]="entity.name"> [entityName]="entity.name">
</tb-attribute-table> </tb-attribute-table>
</mat-tab> </mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id"/>
</mat-tab>
<mat-tab *ngIf="entity" <mat-tab *ngIf="entity"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab"> label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table> <tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>

4
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -69,6 +69,10 @@
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [entityName]="entity.name"/>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit" #auditLogsTab="matTab" <mat-tab *ngIf="entity && !isEdit" #auditLogsTab="matTab"
label="{{ 'audit-log.audit-logs' | translate }}"> label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table> <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>

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

@ -20,6 +20,7 @@ import { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType } from '@shared/models/alias.models';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId { export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId {
debugSettings?: EntityDebugSettings; debugSettings?: EntityDebugSettings;
@ -120,6 +121,7 @@ export interface CalculatedFieldDialogData {
entityId: EntityId; entityId: EntityId;
debugLimitsConfiguration: string; debugLimitsConfiguration: string;
tenantId: string; tenantId: string;
entityName?: string;
} }
export interface ArgumentEntityTypeParams { export interface ArgumentEntityTypeParams {
@ -132,3 +134,23 @@ export const ArgumentEntityTypeParamsMap =new Map<ArgumentEntityType, ArgumentEn
[ArgumentEntityType.Asset, { title: 'calculated-fields.asset-name', entityType: EntityType.ASSET }], [ArgumentEntityType.Asset, { title: 'calculated-fields.asset-name', entityType: EntityType.ASSET }],
[ArgumentEntityType.Customer, { title: 'calculated-fields.customer-name', entityType: EntityType.CUSTOMER }], [ArgumentEntityType.Customer, { title: 'calculated-fields.customer-name', entityType: EntityType.CUSTOMER }],
]) ])
export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entityId: EntityId) => {
switch (entityId.entityType) {
case EntityType.ASSET_PROFILE:
return {
assetTypes: [entityName],
type: AliasFilterType.assetType
};
case EntityType.DEVICE_PROFILE:
return {
deviceTypes: [entityName],
type: AliasFilterType.deviceType
};
default:
return {
type: AliasFilterType.singleEntity,
singleEntity: entityId,
};
}
}

Loading…
Cancel
Save