Browse Source

Merge pull request #14406 from thingsboard/fix/alarm-rules

Improvements for alarm rules
pull/14473/head
Viacheslav Klimov 10 months ago
committed by GitHub
parent
commit
87b800bc47
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 2
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  3. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  4. 8
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  5. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  7. 55
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  8. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java
  9. 27
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  10. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  11. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  12. 2
      application/src/main/resources/thingsboard.yml
  13. 56
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  14. 31
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  15. 20
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  16. 7
      application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java
  17. 5
      application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
  18. 23
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  19. 4
      application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java
  20. 187
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  21. 6
      application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java
  22. 5
      common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java
  23. 6
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java
  24. 17
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java
  25. 5
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java
  26. 19
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java
  27. 6
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java
  28. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java
  29. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java
  30. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java
  31. 43
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java
  32. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java
  33. 7
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java
  34. 2
      ui-ngx/src/app/core/services/menu.models.ts
  35. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
  36. 48
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  37. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html
  38. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  39. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  40. 53
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  41. 157
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  42. 11
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  43. 74
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  44. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  45. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
  46. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts
  47. 33
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  48. 18
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
  49. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  50. 53
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  51. 32
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  52. 13
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
  53. 30
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  54. 16
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  55. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
  56. 26
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
  57. 77
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html
  58. 166
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts
  59. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
  60. 33
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  61. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  62. 122
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  63. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
  64. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
  65. 36
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
  66. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts
  67. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  68. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts
  69. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html
  70. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  71. 4
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html
  72. 11
      ui-ngx/src/app/modules/home/components/vc/entity-types-version.component.scss
  73. 2
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  74. 101
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  75. 2
      ui-ngx/src/app/shared/models/vc.models.ts
  76. 56
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -670,7 +670,7 @@ public class ActorSystemContext {
@Getter @Getter
private long cfCheckInterval; private long cfCheckInterval;
@Value("${actors.alarms.reevaluation_interval:120}") @Value("${actors.alarms.reevaluation_interval:60}")
@Getter @Getter
private long alarmRulesReevaluationInterval; private long alarmRulesReevaluationInterval;

2
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -178,7 +178,7 @@ public class AppActor extends ContextAwareActor {
}); });
} }
} }
if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE)) { if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE, EntityType.USER)) {
log.warn("Message has system tenant id: {}", msg); log.warn("Message has system tenant id: {}", msg);
} }
} else { } else {

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

@ -180,7 +180,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> {
try { try {
calculatedFields.values().forEach(cf -> { calculatedFields.values().forEach(cf -> {
if (cf.isRequiresScheduledReevaluation()) { if (cf.requiresScheduledReevaluation()) {
applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> { applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> {
log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId()); log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId());
getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf)); getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf));

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

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
@ -243,11 +244,12 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
@Override @Override
public EntityId getProfileId(TenantId tenantId, EntityId entityId) { public EntityId getProfileId(TenantId tenantId, EntityId entityId) {
return switch (entityId.getEntityType()) { HasId<? extends EntityId> profile = switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId);
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId);
default -> null; default -> null;
}; };
return profile != null ? profile.getId() : null;
} }
@Override @Override

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

@ -212,7 +212,7 @@ public class CalculatedFieldCtx implements Closeable {
this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024;
} }
public boolean isRequiresScheduledReevaluation() { public boolean requiresScheduledReevaluation() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) { if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) {
Watermark watermark = entityAggregationConfig.getWatermark(); Watermark watermark = entityAggregationConfig.getWatermark();
@ -232,8 +232,8 @@ public class CalculatedFieldCtx implements Closeable {
} }
boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation();
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval());
if (requiresScheduledReevaluation) { if (requiresScheduledReevaluation) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval());
if (now - lastReevaluationTs >= reevaluationIntervalMillis) { if (now - lastReevaluationTs >= reevaluationIntervalMillis) {
lastReevaluationTs = now; lastReevaluationTs = now;
return true; return true;
@ -642,6 +642,9 @@ public class CalculatedFieldCtx implements Closeable {
// if the rules have any changes not tracked by hasStateChanges // if the rules have any changes not tracked by hasStateChanges
return true; return true;
} }
if (!thisConfig.propagationSettingsEqual(otherConfig)) {
return true;
}
} }
if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) { if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) {
return true; return true;

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

@ -43,7 +43,6 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString;
@ -105,11 +104,6 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
this.stateProducer = (TbKafkaProducerTemplate<TbProtoQueueMsg<CalculatedFieldStateProto>>) queueFactory.createCalculatedFieldStateProducer(); this.stateProducer = (TbKafkaProducerTemplate<TbProtoQueueMsg<CalculatedFieldStateProto>>) queueFactory.createCalculatedFieldStateProducer();
} }
@Override
public void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions) {
stateService.update(queueKey, partitions, null);
}
@Override @Override
protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId()); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId());

55
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAl
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
@ -70,6 +71,8 @@ import java.util.function.Function;
import static org.thingsboard.server.common.data.StringUtils.equalsAny; import static org.thingsboard.server.common.data.StringUtils.equalsAny;
import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.NEW_EVENT;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.SCHEDULED_REEVALUATION;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE;
@ -212,9 +215,9 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
state.setDurationCheckFuture(future); state.setDurationCheckFuture(future);
} }
} }
return evalResult; return evalResult.withCause(NEW_EVENT);
} else { } else {
return state.reeval(System.currentTimeMillis(), ctx); return state.reeval(System.currentTimeMillis(), ctx).withCause(SCHEDULED_REEVALUATION);
} }
}, ctx); }, ctx);
return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() return Futures.immediateFuture(AlarmCalculatedFieldResult.builder()
@ -247,12 +250,12 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
private TbAlarmResult createOrClearAlarms(Function<AlarmRuleState, AlarmEvalResult> evalFunction, private TbAlarmResult createOrClearAlarms(Function<AlarmRuleState, AlarmEvalResult> evalFunction,
CalculatedFieldCtx ctx) { CalculatedFieldCtx ctx) {
TbAlarmResult result = null; AlarmEvalResult evalResult = null;
AlarmRuleState resultState = null; AlarmRuleState resultState = null;
AlarmRuleState.StateInfo resultStateInfo = null; AlarmRuleState.StateInfo resultStateInfo = null;
for (AlarmRuleState state : createRuleStates.values()) { for (AlarmRuleState state : createRuleStates.values()) {
AlarmEvalResult evalResult = evalFunction.apply(state); evalResult = evalFunction.apply(state);
log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult); log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult);
if (evalResult.getStatus() == TRUE) { if (evalResult.getStatus() == TRUE) {
resultState = state; resultState = state;
@ -262,13 +265,14 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
} }
} }
TbAlarmResult result = null;
if (resultState != null) { if (resultState != null) {
result = calculateAlarmResult(resultState, ctx); result = calculateAlarmResult(resultState, evalResult, ctx);
resultStateInfo = resultState.getStateInfo(); resultStateInfo = resultState.getStateInfo();
log.debug("Alarm result for state {}: {}", resultState, result); log.debug("Alarm result for state {}: {}", resultState, result);
clearState(clearRuleState); clearState(clearRuleState);
} else if (currentAlarm != null && clearRuleState != null) { } else if (currentAlarm != null && clearRuleState != null) {
AlarmEvalResult evalResult = evalFunction.apply(clearRuleState); evalResult = evalFunction.apply(clearRuleState);
log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult); log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult);
if (evalResult.getStatus() == TRUE) { if (evalResult.getStatus() == TRUE) {
resultStateInfo = clearRuleState.getStateInfo(); resultStateInfo = clearRuleState.getStateInfo();
@ -315,21 +319,25 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
} }
} }
private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, CalculatedFieldCtx ctx) { private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, AlarmEvalResult evalResult, CalculatedFieldCtx ctx) {
AlarmSeverity severity = ruleState.getSeverity(); AlarmSeverity severity = ruleState.getSeverity();
if (currentAlarm != null) { if (currentAlarm != null) {
currentAlarm.setEndTs(System.currentTimeMillis());
AlarmSeverity oldSeverity = currentAlarm.getSeverity(); AlarmSeverity oldSeverity = currentAlarm.getSeverity();
// Skip update if severity is decreased. if (severity.ordinal() > oldSeverity.ordinal()) {
if (severity.ordinal() <= oldSeverity.ordinal()) { log.trace("Skipping alarm update for result state {} for eval result {} because severity is decreased", ruleState, evalResult);
currentAlarm.setDetails(createDetails(ruleState));
currentAlarm.setSeverity(severity);
AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
} else {
return null; return null;
} }
if (severity.ordinal() == oldSeverity.ordinal() && evalResult.getCause() == SCHEDULED_REEVALUATION) {
log.trace("Skipping alarm update for result state {} for eval result {}", ruleState, evalResult);
return null;
}
currentAlarm.setEndTs(System.currentTimeMillis());
currentAlarm.setDetails(createDetails(ruleState));
currentAlarm.setSeverity(severity);
AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
} else { } else {
var newAlarm = new Alarm(); var newAlarm = new Alarm();
newAlarm.setType(alarmType); newAlarm.setType(alarmType);
@ -338,7 +346,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
newAlarm.setSeverity(severity); newAlarm.setSeverity(severity);
long startTs = latestTimestamp; long startTs = latestTimestamp;
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
if (startTs == 0L || startTs > currentTime) { if (startTs <= 0L || startTs > currentTime) {
startTs = currentTime; startTs = currentTime;
} }
newAlarm.setStartTs(startTs); newAlarm.setStartTs(startTs);
@ -428,6 +436,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate); case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate);
case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate); case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate);
case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate); case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate);
case NO_DATA -> evalNoDataPredicate(argument, (NoDataFilterPredicate) predicate);
case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate); case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate);
}; };
} }
@ -512,6 +521,18 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
}; };
} }
private boolean evalNoDataPredicate(SingleValueArgumentEntry argument, NoDataFilterPredicate predicate) {
long passedMs = System.currentTimeMillis() - argument.getTs();
long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue);
if (duration > 0) {
long requiredDuration = predicate.getUnit().toMillis(duration);
log.trace("[{}] No data for argument {} during {} ms, required duration: {} ms", ctx, argument, passedMs, requiredDuration);
return passedMs >= requiredDuration;
} else {
return false;
}
}
protected <T> T resolveValue(AlarmConditionValue<T> conditionValue, Function<KvEntry, T> mapper) { protected <T> T resolveValue(AlarmConditionValue<T> conditionValue, Function<KvEntry, T> mapper) {
T value = conditionValue.getStaticValue(); T value = conditionValue.getStaticValue();
if (value == null) { if (value == null) {

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

@ -30,6 +30,7 @@ public class AlarmEvalResult {
private final Status status; private final Status status;
private final long leftDuration; private final long leftDuration;
private final long leftEvents; private final long leftEvents;
private Cause cause;
public AlarmEvalResult(Status status) { public AlarmEvalResult(Status status) {
this(status, 0, 0); this(status, 0, 0);
@ -39,8 +40,17 @@ public class AlarmEvalResult {
return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents); return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents);
} }
public AlarmEvalResult withCause(Cause cause) {
this.cause = cause;
return this;
}
public enum Status { public enum Status {
FALSE, NOT_YET_TRUE, TRUE; FALSE, NOT_YET_TRUE, TRUE;
} }
public enum Cause {
NEW_EVENT, SCHEDULED_REEVALUATION;
}
} }

27
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java

@ -43,6 +43,8 @@ import java.time.ZonedDateTime;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE;
@Data @Data
@Slf4j @Slf4j
public class AlarmRuleState { public class AlarmRuleState {
@ -81,16 +83,20 @@ public class AlarmRuleState {
boolean active = isActive(ts); boolean active = isActive(ts);
switch (condition.getType()) { switch (condition.getType()) {
case SIMPLE, REPEATING -> { case SIMPLE, REPEATING -> {
if (this.active == null || active != this.active) { boolean activeChanged = this.active == null || active != this.active;
this.active = active; this.active = active;
if (active) { if (!active) {
return doEval(false, ctx); return AlarmEvalResult.EMPTY;
}
} }
if (active) {
return AlarmEvalResult.NOT_YET_TRUE; if ((condition.hasSchedule() && activeChanged) ||
} else { condition.getExpression().requiresScheduledReevaluation()) {
return AlarmEvalResult.FALSE; AlarmEvalResult result = doEval(false, ctx);
if (result.getStatus() == TRUE) {
return result;
} else {
return AlarmEvalResult.EMPTY;
}
} }
} }
case DURATION -> { case DURATION -> {
@ -116,7 +122,7 @@ public class AlarmRuleState {
} }
} }
} }
return AlarmEvalResult.FALSE; return AlarmEvalResult.EMPTY;
} }
public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) { public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) {
@ -338,6 +344,7 @@ public class AlarmRuleState {
} }
public record StateInfo(Long eventCount, Long duration) { public record StateInfo(Long eventCount, Long duration) {
static final StateInfo EMPTY = new StateInfo(null, null); static final StateInfo EMPTY = new StateInfo(null, null);
} }

9
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.sync.ie.exporting.impl; package org.thingsboard.server.service.sync.ie.exporting.impl;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -170,6 +172,13 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
}); });
} }
} }
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration alarmCfConfig) {
alarmCfConfig.getAllRules().map(Pair::getValue).forEach(rule -> {
if (rule.getDashboardId() != null) {
rule.setDashboardId(getExternalIdOrElseInternal(ctx, rule.getDashboardId()));
}
});
}
}); });
return calculatedFields; return calculatedFields;
} }

9
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -22,6 +22,7 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -338,6 +340,13 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}); });
} }
} }
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration alarmCfConfig) {
alarmCfConfig.getAllRules().map(Pair::getValue).forEach(rule -> {
if (rule.getDashboardId() != null) {
rule.setDashboardId(idProvider.getInternalId(rule.getDashboardId(), ctx.isFinalImportAttempt()));
}
});
}
}).toList(); }).toList();
for (CalculatedField existingField : existing) { for (CalculatedField existingField : existing) {

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

@ -542,7 +542,7 @@ actors:
check_interval: "${ACTORS_CALCULATED_FIELDS_CHECK_INTERVAL_SEC:60}" check_interval: "${ACTORS_CALCULATED_FIELDS_CHECK_INTERVAL_SEC:60}"
alarms: alarms:
# Interval in seconds to re-evaluate Alarm rules that have a time schedule. 2 minutes by default. # Interval in seconds to re-evaluate Alarm rules that have a time schedule. 2 minutes by default.
reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:120}" reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:60}"
debug: debug:
settings: settings:

56
application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmC
import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate;
@ -475,6 +476,61 @@ public class AlarmRulesTest extends AbstractControllerTest {
}); });
} }
@Test
public void testCreateAlarm_noDataPredicate() throws Exception {
Argument temperatureArgument = new Argument();
temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
temperatureArgument.setDefaultValue("0");
Map<String, Argument> arguments = Map.of(
"temperature", temperatureArgument
);
long majorNoDataDuration = 3L;
long criticalNoDataDuration = 10L;
SimpleAlarmConditionExpression majorExpression = new SimpleAlarmConditionExpression();
AlarmConditionFilter majorFilter = new AlarmConditionFilter();
majorFilter.setArgument("temperature");
majorFilter.setValueType(EntityKeyValueType.NUMERIC);
majorFilter.setPredicates(List.of(
new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)),
new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue<Long>(majorNoDataDuration, null))
));
majorExpression.setFilters(List.of(majorFilter));
SimpleAlarmConditionExpression criticalExpression = new SimpleAlarmConditionExpression();
AlarmConditionFilter criticalFilter = new AlarmConditionFilter();
criticalFilter.setArgument("temperature");
criticalFilter.setValueType(EntityKeyValueType.NUMERIC);
criticalFilter.setPredicates(List.of(
new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)),
new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue<Long>(criticalNoDataDuration, null))
));
criticalExpression.setFilters(List.of(criticalFilter));
Map<AlarmSeverity, Condition> createRules = Map.of(
AlarmSeverity.MAJOR, new Condition(majorExpression, null, null),
AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
}
@Test @Test
public void testChangeAlarmType() throws Exception { public void testChangeAlarmType() throws Exception {
Argument temperatureArgument = new Argument(); Argument temperatureArgument = new Argument();

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

@ -84,6 +84,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -93,6 +94,7 @@ import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
@ -1207,8 +1209,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states"); Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states");
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId); CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() <
< System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady; return isReady;
}); });
@ -1399,7 +1401,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected List<Job> findJobs(List<JobType> types, List<UUID> entities) throws Exception { protected List<Job> findJobs(List<JobType> types, List<UUID> entities) throws Exception {
return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) +
"&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&",
new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData();
} }
@ -1413,12 +1415,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected void postTelemetry(EntityId entityId, String payload) throws Exception { protected void postTelemetry(EntityId entityId, String payload) throws Exception {
doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() +
"/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
} }
protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception {
doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() +
"/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk());
} }
protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { protected CalculatedField saveCalculatedField(CalculatedField calculatedField) {
@ -1427,7 +1429,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" +
(type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink);
}
protected PageData<String> getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&",
new TypeReference<PageData<String>>() {}, pageLink);
}
protected List<CalculatedFieldInfo> getCalculatedFields(CalculatedFieldType type,
EntityType entityType,
List<UUID> entities,
List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" +
(entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") +
(names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""),
new TypeReference<PageData<CalculatedFieldInfo>>() {}, new PageLink(10)).getData();
} }
protected PageData<EventInfo> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { protected PageData<EventInfo> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {

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

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -56,9 +55,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -295,23 +292,6 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(names.getData()).containsOnly(deviceCalculatedField.getName()); assertThat(names.getData()).containsOnly(deviceCalculatedField.getName());
} }
private PageData<String> getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&",
new TypeReference<PageData<String>>() {}, pageLink);
}
private List<CalculatedFieldInfo> getCalculatedFields(CalculatedFieldType type,
EntityType entityType,
List<UUID> entities,
List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" +
(entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") +
(names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""),
new TypeReference<PageData<CalculatedFieldInfo>>() {}, new PageLink(10)).getData();
}
@Test @Test
public void testDeleteCalculatedField() throws Exception { public void testDeleteCalculatedField() throws Exception {
Device testDevice = createDevice("Test device", "1234567890"); Device testDevice = createDevice("Test device", "1234567890");

7
application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java

@ -22,6 +22,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -62,8 +64,11 @@ public class EdqsControllerTest extends AbstractControllerTest {
@Autowired @Autowired
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@Before @Before
public void beforeEdqsControllerTest() throws Exception { public void before() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
} }

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

@ -19,7 +19,6 @@ import org.assertj.core.api.ThrowingConsumer;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode;
@ -33,7 +32,6 @@ import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import org.thingsboard.server.queue.discovery.DiscoveryService; import org.thingsboard.server.queue.discovery.DiscoveryService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -59,9 +57,6 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
@Autowired @Autowired
private DiscoveryService discoveryService; private DiscoveryService discoveryService;
@MockBean // so that we don't do backup for tests
private EdqsRocksDb edqsRocksDb;
@Before @Before
public void before() { public void before() {
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.getState().isApiEnabled()); await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.getState().isApiEnabled());

23
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -23,6 +23,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -71,6 +72,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -102,6 +104,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
@Autowired @Autowired
private QueueStatsService queueStatsService; private QueueStatsService queueStatsService;
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@Before @Before
public void beforeTest() throws Exception { public void beforeTest() throws Exception {
loginSysAdmin(); loginSysAdmin();
@ -436,7 +441,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET); assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList(); List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList();
@ -446,8 +451,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter); List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields);
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
}); });
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
} }
@ -509,16 +514,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET); assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList());
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter); List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList());
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
}); });
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
} }
@ -574,7 +579,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter(); EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE); deviceTypeFilter.setEntityType(EntityType.DEVICE);
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
@ -1291,7 +1296,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
findByQueryAndCheck(query, 0); findByQueryAndCheck(query, 0);
} }
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer<Integer,EntityData> checkFunction) throws Exception { private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer<Integer, EntityData> checkFunction) throws Exception {
await() await()
.alias("data by query") .alias("data by query")
.atMost(30, TimeUnit.SECONDS) .atMost(30, TimeUnit.SECONDS)

4
application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java

@ -19,8 +19,8 @@ import com.google.common.collect.Lists;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -61,7 +61,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest {
@Autowired @Autowired
private EdqsService edqsService; private EdqsService edqsService;
@MockBean @MockitoBean
private EdqsRocksDb edqsRocksDb; private EdqsRocksDb edqsRocksDb;
@Before @Before

187
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -45,10 +46,15 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
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;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument; 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.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
@ -306,6 +312,54 @@ public class VersionControlTest extends AbstractControllerTest {
checkImportedOtaPackageData(software, importedSoftwareOta); checkImportedOtaPackageData(software, importedSoftwareOta);
} }
@Test
public void testDeviceVc_withAlarmRules_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1");
Dashboard dashboard = createDashboard(null, "Mobile dashboard");
createAlarmRule(deviceProfile.getId(), "Profile alarm rule", dashboard.getId());
Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1");
createAlarmRule(device.getId(), "Device alarm rule", dashboard.getId());
String version = createVersion("devices, profiles and dashboards", EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD);
loginTenant2();
Map<EntityType, EntityTypeLoadResult> result = loadVersion(version, config -> {
config.setLoadCredentials(false);
}, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD);
assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1);
assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1);
assertThat(result.get(EntityType.DASHBOARD).getCreated()).isEqualTo(1);
Device importedDevice = findDevice(device.getName());
checkImportedEntity(tenantId1, device, tenantId2, importedDevice);
checkImportedDeviceData(device, importedDevice);
DeviceProfile importedDeviceProfile = findDeviceProfile(deviceProfile.getName());
checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedDeviceProfile);
checkImportedDeviceProfileData(deviceProfile, importedDeviceProfile);
assertThat(importedDevice.getDeviceProfileId()).isEqualTo(importedDeviceProfile.getId());
Dashboard importedDashboard = findDashboard(dashboard.getName());
checkImportedEntity(tenantId1, dashboard, tenantId2, importedDashboard);
checkImportedDashboardData(dashboard, importedDashboard);
getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE,
List.of(importedDeviceProfile.getUuidId()), null).forEach(alarmRuleCf -> {
assertThat(alarmRuleCf.getName()).isEqualTo("Profile alarm rule");
AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration();
config.getAllRules().map(Pair::getValue).forEach(alarmRule -> {
assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId());
});
});
getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE,
List.of(importedDevice.getUuidId()), null).forEach(alarmRuleCf -> {
assertThat(alarmRuleCf.getName()).isEqualTo("Device alarm rule");
AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration();
config.getAllRules().map(Pair::getValue).forEach(alarmRule -> {
assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId());
});
});
}
@Test @Test
public void testDashboardVc_betweenTenants() throws Exception { public void testDashboardVc_betweenTenants() throws Exception {
Dashboard dashboard = createDashboard(null, "Dashboard of tenant 1"); Dashboard dashboard = createDashboard(null, "Dashboard of tenant 1");
@ -368,42 +422,42 @@ public class VersionControlTest extends AbstractControllerTest {
String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c";
String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa"; String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa";
String entityAliases = "{\n" + String entityAliases = "{\n" +
"\"" + aliasId + "\": {\n" + "\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" + "\"alias\": \"assets\",\n" +
"\"filter\": {\n" + "\"filter\": {\n" +
" \"entityList\": [\n" + " \"entityList\": [\n" +
" \"" + asset1.getId() + "\",\n" + " \"" + asset1.getId() + "\",\n" +
" \"" + asset2.getId() + "\",\n" + " \"" + asset2.getId() + "\",\n" +
" \"" + tenantId1.getId() + "\",\n" + " \"" + tenantId1.getId() + "\",\n" +
" \"" + existingDeviceProfile.getId() + "\",\n" + " \"" + existingDeviceProfile.getId() + "\",\n" +
" \"" + unknownUuid + "\"\n" + " \"" + unknownUuid + "\"\n" +
" ],\n" + " ],\n" +
" \"id\":\"" + asset1.getId() + "\",\n" + " \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" + " \"resolveMultiple\": true\n" +
"},\n" + "},\n" +
"\"id\": \"" + aliasId + "\"\n" + "\"id\": \"" + aliasId + "\"\n" +
"}\n" + "}\n" +
"}"; "}";
String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b"; String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b";
String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae"; String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae";
String widgets = "{\n" + String widgets = "{\n" +
" \"" + widgetId + "\": {\n" + " \"" + widgetId + "\": {\n" +
" \"config\": {\n" + " \"config\": {\n" +
" \"actions\": {\n" + " \"actions\": {\n" +
" \"rowClick\": [\n" + " \"rowClick\": [\n" +
" {\n" + " {\n" +
" \"name\": \"go to dashboard\",\n" + " \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" + " \"id\": \"" + actionId + "\"\n" +
" }\n" + " }\n" +
" ]\n" + " ]\n" +
" }\n" + " }\n" +
" },\n" + " },\n" +
" \"row\": 0,\n" + " \"row\": 0,\n" +
" \"col\": 0,\n" + " \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" + " \"id\": \"" + widgetId + "\"\n" +
" }\n" + " }\n" +
"}"; "}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
@ -499,11 +553,12 @@ public class VersionControlTest extends AbstractControllerTest {
generatorNodeConfig.setMsgCount(1); generatorNodeConfig.setMsgCount(1);
generatorNodeConfig.setScriptLang(ScriptLanguage.JS); generatorNodeConfig.setScriptLang(ScriptLanguage.JS);
UUID someUuid = UUID.randomUUID(); UUID someUuid = UUID.randomUUID();
generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" + generatorNodeConfig.setJsScript("""
"var metadata = { data: 40 };\n" + var msg = { temp: 42, humidity: 77 };
"var msgType = \"POST_TELEMETRY_REQUEST\";\n" + var metadata = { data: 40 };
"var someUuid = \"" + someUuid + "\";\n" + var msgType = "POST_TELEMETRY_REQUEST";
"return { msg: msg, metadata: metadata, msgType: msgType };"); var someUuid = "%s";
return { msg: msg, metadata: metadata, msgType: msgType };""".formatted(someUuid));
generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig)); generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig));
nodes.add(generatorNode); nodes.add(generatorNode);
metaData.setNodes(nodes); metaData.setNodes(nodes);
@ -1018,20 +1073,21 @@ public class VersionControlTest extends AbstractControllerTest {
protected Dashboard createDashboard(CustomerId customerId, String name, AssetId assetForEntityAlias) { protected Dashboard createDashboard(CustomerId customerId, String name, AssetId assetForEntityAlias) {
Dashboard dashboard = createDashboard(customerId, name); Dashboard dashboard = createDashboard(customerId, name);
String entityAliases = "{\n" + String entityAliases = """
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + {
"\t\t\"alias\": \"assets\",\n" + "23c4185d-1497-9457-30b2-6d91e69a5b2c": {
"\t\t\"filter\": {\n" + "alias": "assets",
"\t\t\t\"entityList\": [\n" + "filter": {
"\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" + "entityList": [
"\t\t\t],\n" + "%s"
"\t\t\t\"entityType\": \"ASSET\",\n" + ],
"\t\t\t\"resolveMultiple\": true,\n" + "entityType": "ASSET",
"\t\t\t\"type\": \"entityList\"\n" + "resolveMultiple": true,
"\t\t},\n" + "type": "entityList"
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + },
"\t}\n" + "id": "23c4185d-1497-9457-30b2-6d91e69a5b2c"
"}"; }
}""".formatted(assetForEntityAlias.getId().toString());
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("description", new TextNode("hallo")); dashboardConfiguration.set("description", new TextNode("hallo"));
@ -1134,6 +1190,33 @@ public class VersionControlTest extends AbstractControllerTest {
return doPost("/api/calculatedField", calculatedField, CalculatedField.class); return doPost("/api/calculatedField", calculatedField, CalculatedField.class);
} }
private CalculatedField createAlarmRule(EntityId entityId, String alarmType, DashboardId mobileDashboardId) {
Argument temperatureArgument = new Argument();
temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
temperatureArgument.setDefaultValue("0");
Map<String, Argument> arguments = Map.of(
"temperature", temperatureArgument
);
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setName(alarmType);
calculatedField.setType(CalculatedFieldType.ALARM);
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
configuration.setArguments(arguments);
SimpleAlarmCondition createCondition = new SimpleAlarmCondition();
createCondition.setExpression(new TbelAlarmConditionExpression("return temperature >= 50;"));
configuration.setCreateRules(Map.of(
AlarmSeverity.CRITICAL, new AlarmRule(createCondition, "", mobileDashboardId)
));
SimpleAlarmCondition clearCondition = new SimpleAlarmCondition();
clearCondition.setExpression(new TbelAlarmConditionExpression("return temperature < 50;"));
configuration.setClearRule(new AlarmRule(clearCondition, "", mobileDashboardId));
calculatedField.setConfiguration(configuration);
calculatedField.setDebugSettings(DebugSettings.all());
return saveCalculatedField(calculatedField);
}
private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();

6
application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java

@ -162,7 +162,7 @@ class DefaultTelemetrySubscriptionServiceTest {
apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
lenient().when(apiUsageStateService.getApiUsageState(tenantId)).thenReturn(apiUsageState); lenient().when(apiUsageStateService.getApiUsageState(tenantId)).thenReturn(apiUsageState);
lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi); lenient().when(partitionService.resolve(eq(ServiceType.TB_CORE), eq(tenantId), any())).thenReturn(tpi);
lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null))); lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null)));
@ -310,8 +310,6 @@ class DefaultTelemetrySubscriptionServiceTest {
given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView))); given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView)));
// mock that save latest call for entity view is successful // mock that save latest call for entity view is successful
given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
// mock TPI for entity view
given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi);
var request = TimeseriesSaveRequest.builder() var request = TimeseriesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -373,8 +371,6 @@ class DefaultTelemetrySubscriptionServiceTest {
lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView))); lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView)));
// mock that save latest call for entity view is successful // mock that save latest call for entity view is successful
lenient().when(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); lenient().when(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
// mock TPI for entity view
lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).thenReturn(tpi);
var request = TimeseriesSaveRequest.builder() var request = TimeseriesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)

5
common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java

@ -70,11 +70,10 @@ public class TbCaffeineCacheConfiguration {
} }
private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) {
Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder()
final Caffeine<Object, Object> caffeineBuilder
= Caffeine.newBuilder()
.weigher(collectionSafeWeigher()) .weigher(collectionSafeWeigher())
.maximumWeight(cacheSpec.getMaxSize()) .maximumWeight(cacheSpec.getMaxSize())
.recordStats()
.ticker(ticker()); .ticker(ticker());
if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) {
caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES);

6
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java

@ -18,11 +18,15 @@ package org.thingsboard.server.common.data.alarm.rule;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
public class AlarmRule { public class AlarmRule {
@Valid @Valid
@ -33,7 +37,7 @@ public class AlarmRule {
@JsonIgnore @JsonIgnore
public boolean requiresScheduledReevaluation() { public boolean requiresScheduledReevaluation() {
return condition.hasSchedule(); return condition.requiresScheduledReevaluation();
} }
} }

17
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java

@ -21,9 +21,10 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule;
@ -50,6 +51,20 @@ public abstract class AlarmCondition {
return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule);
} }
@JsonIgnore
public boolean requiresScheduledReevaluation() {
return hasSchedule() || expression.requiresScheduledReevaluation();
}
@JsonIgnore
@AssertTrue(message = "Expressions requiring scheduled reevaluation can only be used with simple alarm conditions")
public boolean isValid() {
if (getType() != AlarmConditionType.SIMPLE && expression.requiresScheduledReevaluation()) {
return false;
}
return true;
}
@JsonIgnore @JsonIgnore
public abstract AlarmConditionType getType(); public abstract AlarmConditionType getType();

5
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java

@ -32,4 +32,9 @@ public interface AlarmConditionExpression {
@JsonIgnore @JsonIgnore
AlarmConditionExpressionType getType(); AlarmConditionExpressionType getType();
@JsonIgnore
default boolean requiresScheduledReevaluation() {
return false;
}
} }

19
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java

@ -17,8 +17,11 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.EntityKeyValueType;
@ -34,7 +37,21 @@ public class AlarmConditionFilter implements Serializable {
private EntityKeyValueType valueType; private EntityKeyValueType valueType;
private ComplexOperation operation; private ComplexOperation operation;
@Valid @Valid
@NotNull @NotEmpty
private List<KeyFilterPredicate> predicates; private List<KeyFilterPredicate> predicates;
public boolean hasPredicate(FilterPredicateType type) {
return containsPredicate(predicates, type);
}
private boolean containsPredicate(List<KeyFilterPredicate> predicates, FilterPredicateType type) {
return predicates.stream().anyMatch(predicate -> {
if (predicate instanceof ComplexFilterPredicate complexPredicate) {
return containsPredicate(complexPredicate.getPredicates(), type);
} else {
return predicate.getType() == type;
}
});
}
} }

6
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java

@ -20,6 +20,7 @@ import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType;
import java.util.List; import java.util.List;
@ -38,4 +39,9 @@ public class SimpleAlarmConditionExpression implements AlarmConditionExpression
return AlarmConditionExpressionType.SIMPLE; return AlarmConditionExpressionType.SIMPLE;
} }
@Override
public boolean requiresScheduledReevaluation() {
return filters.stream().anyMatch(filter -> filter.hasPredicate(FilterPredicateType.NO_DATA));
}
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java

@ -16,9 +16,13 @@
package org.thingsboard.server.common.data.alarm.rule.condition.expression; package org.thingsboard.server.common.data.alarm.rule.condition.expression;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
public class TbelAlarmConditionExpression implements AlarmConditionExpression { public class TbelAlarmConditionExpression implements AlarmConditionExpression {
@NotBlank @NotBlank

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java

@ -19,5 +19,6 @@ public enum FilterPredicateType {
STRING, STRING,
NUMERIC, NUMERIC,
BOOLEAN, BOOLEAN,
NO_DATA,
COMPLEX COMPLEX
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java

@ -27,7 +27,9 @@ import java.io.Serializable;
@Type(value = StringFilterPredicate.class, name = "STRING"), @Type(value = StringFilterPredicate.class, name = "STRING"),
@Type(value = NumericFilterPredicate.class, name = "NUMERIC"), @Type(value = NumericFilterPredicate.class, name = "NUMERIC"),
@Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), @Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"),
@Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) @Type(value = NoDataFilterPredicate.class, name = "NO_DATA"),
@Type(value = ComplexFilterPredicate.class, name = "COMPLEX")
})
public interface KeyFilterPredicate extends Serializable { public interface KeyFilterPredicate extends Serializable {
@JsonIgnore @JsonIgnore

43
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java

@ -0,0 +1,43 @@
/**
* 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.common.data.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
import java.util.concurrent.TimeUnit;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NoDataFilterPredicate implements KeyFilterPredicate {
@NotNull
private TimeUnit unit;
@Valid
@NotNull
private AlarmConditionValue<Long> duration;
@Override
public FilterPredicateType getType() {
return FilterPredicateType.NO_DATA;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java

@ -17,10 +17,14 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> { public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> {
@NotNull @NotNull

7
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java

@ -87,4 +87,11 @@ public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculat
}); });
} }
public boolean propagationSettingsEqual(AlarmCalculatedFieldConfiguration other) {
return this.propagate == other.propagate &&
this.propagateToOwner == other.propagateToOwner &&
this.propagateToTenant == other.propagateToTenant &&
Objects.equals(this.propagateRelationTypes, other.propagateRelationTypes);
}
} }

2
ui-ngx/src/app/core/services/menu.models.ts

@ -510,7 +510,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
MenuId.alarms, MenuId.alarms,
{ {
id: MenuId.alarms, id: MenuId.alarms,
name: 'alarm.alarms', name: 'alarm.alarm-list',
type: 'link', type: 'link',
path: '/alarms/alarms', path: '/alarms/alarms',
icon: 'mdi:alert-outline' icon: 'mdi:alert-outline'

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html

@ -27,8 +27,10 @@
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<textarea matInput [formControl]="alarmDetailsControl" rows="5" <textarea matInput [formControl]="alarmDetailsControl" rows="5"
placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea> placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
<mat-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint>
</mat-form-field> </mat-form-field>
<div class="tb-form-hint tb-primary-fill flex items-center gap-2">
<span [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></span>
</div>
</div> </div>
</div> </div>
<div mat-dialog-actions class="justify-end"> <div mat-dialog-actions class="justify-end">

48
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -130,30 +130,32 @@
{{ 'alarm-rule.advanced-settings' | translate }} {{ 'alarm-rule.advanced-settings' | translate }}
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="tb-form-row"> <div class="tb-form-panel stroked no-padding no-gap">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate"> <div class="tb-form-row no-border">
{{ 'alarm-rule.propagate-alarm' | translate }} <mat-slide-toggle class="mat-slide margin" formControlName="propagate">
</mat-slide-toggle> {{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block p-4" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
}
</div> </div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
}
<div class="tb-form-row"> <div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner"> <mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner">
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }} {{ 'alarm-rule.propagate-alarm-to-owner' | translate }}

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html

@ -66,7 +66,7 @@
</tb-entity-subtype-list> </tb-entity-subtype-list>
</div> </div>
<div class="tb-form-row column-xs"> <div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.entity-type</div> <div class="fixed-title-width" translate>alarm-rule.target-entity-type</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}"> <mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option> <mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
@ -78,7 +78,7 @@
</div> </div>
@if (alarmRuleFilterConfigForm.get('entityType').value) { @if (alarmRuleFilterConfigForm.get('entityType').value) {
<div class="tb-form-row column-xs"> <div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.alarm-rule-entity-list</div> <div class="fixed-title-width" translate>alarm-rule.target-entities</div>
<tb-entity-list appearance="outline" <tb-entity-list appearance="outline"
subscriptSizing="dynamic" subscriptSizing="dynamic"
class="flex flex-1" class="flex flex-1"

6
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts

@ -50,6 +50,9 @@ import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/fil
import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
import { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.component"; import { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.component";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
import {
AlarmRuleFilterPredicateNoDataValueComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -69,7 +72,8 @@ import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alar
AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent, AlarmRuleDetailsDialogComponent,
AlarmRuleFilterConfigComponent, AlarmRuleFilterConfigComponent,
AlarmRuleTableHeaderComponent AlarmRuleTableHeaderComponent,
AlarmRuleFilterPredicateNoDataValueComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

6
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -138,16 +138,16 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%', this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%',
entity => this.utilsService.customTranslation(entity.name, entity.name))); entity => this.utilsService.customTranslation(entity.name, entity.name)));
if (this.pageMode) { if (this.pageMode) {
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('entityType', 'alarm-rule.entity-type', '15%', this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('entityType', 'alarm-rule.target-entity-type', '15%',
entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)));
this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'alarm-rule.entity-name', '30%', this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'alarm-rule.target-entity', '30%',
entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']),
entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false));
} }
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%', this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%',
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
() => ({}), false)); () => ({}), false));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '70px', this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '90px',
entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false));
this.cellActionDescriptors.push( this.cellActionDescriptors.push(

53
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html

@ -90,15 +90,47 @@
</div> </div>
} }
</div> </div>
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="true">
<mat-expansion-panel-header>
{{ 'alarm-rule.filter-preview' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div>
@if (specText) {
<span class="spec-text">{{ specText }}</span>
}
@if (conditionFormGroup.get('expression.filters').value?.length) {
<tb-alarm-rule-filter-text [alarmRuleExpression]="conditionFormGroup.get('expression').value"
[arguments]="arguments"
class="flex-1"
[nowrap]="false"
required>
</tb-alarm-rule-filter-text>
} @else {
<span class="tb-prompt justify-center">{{ 'alarm-rule.no-filter-preview' | translate }}</span>
}
</div>
</ng-template>
</mat-expansion-panel>
</div>
}
<section class="tb-form-panel"> <section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div> <div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.condition-type</mat-label> <mat-label translate>alarm-rule.condition-type</mat-label>
<mat-select formControlName="type" required> <mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"
[disabled]="isNoData ? alarmConditionType === AlarmConditionType.REPEATING || alarmConditionType === AlarmConditionType.DURATION : false">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
@if (isNoData) {
<mat-hint translate>alarm-rule.condition-type-hint</mat-hint>
}
</mat-form-field> </mat-form-field>
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) { @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
<div class="tb-form-panel stroked no-padding-bottom"> <div class="tb-form-panel stroked no-padding-bottom">
@ -146,7 +178,7 @@
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container> <ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div> </div>
<div class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value"> <div class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container> <ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div> </div>
</div> </div>
</div> </div>
@ -162,9 +194,8 @@
</button> </button>
@if (!readonly) { @if (!readonly) {
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit" type="submit"
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty"> [disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty || !filtersValid">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>
} }
@ -186,6 +217,9 @@
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) {
<mat-error>{{ defaultValuePatternError | translate }}</mat-error> <mat-error>{{ defaultValuePatternError | translate }}</mat-error>
} }
@if (type === AlarmConditionType.REPEATING) {
<mat-hint translate>alarm-rule.condition-repeating-value-hint</mat-hint>
}
</mat-form-field> </mat-form-field>
</div> </div>
</ng-template> </ng-template>
@ -198,9 +232,14 @@
<mat-option [value]="argument">{{ argument }}</mat-option> <mat-option [value]="argument">{{ argument }}</mat-option>
} }
</mat-select> </mat-select>
<mat-error *ngIf="conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')"> @if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) {
{{ 'calculated-fields.hint.argument-name-required' | translate }} <mat-error>
</mat-error> {{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
}
@if (type === AlarmConditionType.REPEATING) {
<mat-hint translate>alarm-rule.condition-repeating-value-hint</mat-hint>
}
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>
</ng-template> </ng-template>

157
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts

@ -24,13 +24,6 @@ import { DialogComponent } from '@app/shared/components/dialog.component';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from "@shared/models/rule-node.models"; import { ScriptLanguage } from "@shared/models/rule-node.models";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType
} from "@shared/models/alarm-rule.models";
import { import {
CalculatedFieldArgument, CalculatedFieldArgument,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
@ -38,8 +31,18 @@ import {
} from "@shared/models/calculated-field.models"; } from "@shared/models/calculated-field.models";
import { TbEditorCompleter } from "@shared/models/ace/completion.models"; import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models"; import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; import { ComplexOperation } from "@shared/models/query/query.models";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { TranslateService } from "@ngx-translate/core";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType,
AlarmRuleFilter,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
export interface CfAlarmRuleConditionDialogData { export interface CfAlarmRuleConditionDialogData {
readonly: boolean; readonly: boolean;
@ -96,7 +99,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
repeatingDynamicModeControl = this.fb.control<boolean>(false); repeatingDynamicModeControl = this.fb.control<boolean>(false);
ComplexOperation = ComplexOperation; ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap; complexOperationTranslationMap = filterOperationTranslationMap;
specText = '';
filtersValid: boolean = false;
functionArgs: Array<string>; functionArgs: Array<string>;
argumentsEditorCompleter: TbEditorCompleter; argumentsEditorCompleter: TbEditorCompleter;
@ -105,11 +112,14 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
arguments = this.data.arguments; arguments = this.data.arguments;
argumentsList: Array<string>; argumentsList: Array<string>;
isNoData: boolean = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>, public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>,
private fb: FormBuilder) { private fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
@ -128,21 +138,38 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
unit: this.condition?.unit ?? TimeUnit.SECONDS, unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: { value: {
staticValue: this.condition?.value?.staticValue, staticValue: this.condition?.value?.staticValue,
dynamicValueArgument: this.condition?.value?.dynamicValueArgument, dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.value?.dynamicValueArgument) ? this.condition?.value?.dynamicValueArgument : null,
}, },
count: { count: {
staticValue: this.condition?.count?.staticValue ?? null, staticValue: this.condition?.count?.staticValue ?? null,
dynamicValueArgument: this.condition?.count?.dynamicValueArgument dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.count?.dynamicValueArgument) ? this.condition?.count?.dynamicValueArgument : null
} }
}, {emitEvent: false}); }, {emitEvent: false});
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false}); this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false}); this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false});
this.filtersValid = this.areFilterAndPredicateArgumentsValid(this.condition?.expression?.filters, this.argumentsList);
this.checkIsNoData(this.condition?.expression?.filters);
this.conditionFormGroup.get('type').valueChanges.pipe( this.conditionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
).subscribe((type) => { ).subscribe((type) => {
this.updateValidators(type, true); this.updateValidators(type);
});
this.conditionFormGroup.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value) => {
this.updateSpecText(value.type);
})
this.conditionFormGroup.get('expression.filters').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((filters) => {
this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList);
this.checkIsNoData(filters);
}); });
this.conditionFormGroup.get('expression.type').valueChanges.pipe( this.conditionFormGroup.get('expression.type').valueChanges.pipe(
@ -164,6 +191,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
}); });
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateSpecText(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
} }
@ -178,6 +206,39 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
} }
} }
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
const filters = obj || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
}
return true;
}
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') { updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
if (type === 'SIMPLE') { if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false}); this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
@ -188,7 +249,26 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
} }
} }
private updateValidators(type: AlarmRuleConditionType, emitEvent = false) { private checkIsNoData(filters: Array<AlarmRuleFilter>) {
this.isNoData = this.hasNoData(filters);
if (this.isNoData && this.conditionFormGroup.get('type').value !== AlarmRuleConditionType.SIMPLE) {
this.conditionFormGroup.get('type').patchValue(AlarmRuleConditionType.SIMPLE);
}
}
private hasNoData(data: Array<AlarmRuleFilter>) {
const search = (filter) => {
if (!filter) return false;
if (Array.isArray(filter)) return filter.some(search);
if (typeof filter !== 'object') return false;
if (filter.type === 'NO_DATA') return true;
if (filter.predicates?.length) return filter.predicates.some(search);
return false;
};
return search(data);
};
private updateValidators(type: AlarmRuleConditionType) {
switch (type) { switch (type) {
case AlarmRuleConditionType.DURATION: case AlarmRuleConditionType.DURATION:
this.conditionFormGroup.get('unit').enable({emitEvent: false}); this.conditionFormGroup.get('unit').enable({emitEvent: false});
@ -218,6 +298,55 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
} }
} }
private updateSpecText(type: AlarmRuleConditionType) {
this.specText = '';
const value = this.conditionFormGroup.get('value').value;
const count = this.conditionFormGroup.get('count').value;
switch (type) {
case AlarmRuleConditionType.SIMPLE:
break;
case AlarmRuleConditionType.DURATION:
let duringText = '';
switch (this.conditionFormGroup.get('unit').value) {
case TimeUnit.SECONDS:
duringText = this.translate.instant('timewindow.seconds', {seconds: value.staticValue});
break;
case TimeUnit.MINUTES:
duringText = this.translate.instant('timewindow.minutes', {minutes: value.staticValue});
break;
case TimeUnit.HOURS:
duringText = this.translate.instant('timewindow.hours', {hours: value.staticValue});
break;
case TimeUnit.DAYS:
duringText = this.translate.instant('timewindow.days', {days: value.staticValue});
break;
}
if (value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${value.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText
});
}
break;
case AlarmRuleConditionType.REPEATING:
if (count.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', {
attribute: `${count.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times',
{count: count.staticValue});
}
break;
}
if (this.specText.length > 0) {
this.specText = this.specText + ':';
}
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }

11
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html

@ -21,7 +21,7 @@
<button [disabled]="disabled" <button [disabled]="disabled"
type="button" type="button"
class="tb-alarm-rule-condition-button" class="tb-alarm-rule-condition-button"
mat-stroked-button [color]="conditionSet() ? 'primary' : 'warn'" mat-stroked-button [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'"
(click)="openFilterDialog($event)"> (click)="openFilterDialog($event)">
<div class="flex items-center gap-2 justify-between"> <div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value" <tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
@ -32,20 +32,23 @@
required required
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}"> addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</tb-alarm-rule-filter-text> </tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon> <mat-icon [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
{{ conditionSet() ? 'edit' : 'add' }}
</mat-icon>
</div> </div>
</button> </button>
</div> </div>
<div class="tb-form-row column-xs"> <div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div> <div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div>
<button [disabled]="disabled" <button [disabled]="disabled"
[color]="schedulerArgumentsValid ? 'primary' : 'warn'"
type="button" type="button"
class="tb-alarm-rule-condition-button" class="tb-alarm-rule-condition-button"
mat-stroked-button color="primary" mat-stroked-button
(click)="openScheduleDialog($event)"> (click)="openScheduleDialog($event)">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span> <span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span>
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon> <mat-icon [color]="schedulerArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
</div> </div>
</button> </button>
</div> </div>

74
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts

@ -14,8 +14,9 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { import {
AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
@ -68,7 +69,7 @@ import { Observable } from "rxjs";
} }
] ]
}) })
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator { export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges {
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -96,11 +97,15 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
specText = ''; specText = '';
filtersArgumentsValid: boolean = true;
schedulerArgumentsValid: boolean = true;
scheduleText = ''; scheduleText = '';
private modelValue: AlarmRuleCondition; private modelValue: AlarmRuleCondition;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
private onValidatorChange = () => { };
constructor(private dialog: MatDialog, constructor(private dialog: MatDialog,
private fb: FormBuilder, private fb: FormBuilder,
@ -115,6 +120,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
registerOnValidatorChange(fn: () => void): void {
this.onValidatorChange = fn;
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
@ -127,14 +136,68 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
writeValue(value: AlarmRuleCondition): void { writeValue(value: AlarmRuleCondition): void {
this.modelValue = value; this.modelValue = value;
this.updateConditionInfo(); this.updateConditionInfo();
if (value) {
this.onValidatorChange();
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.arguments) {
if (changes.arguments && !changes.arguments.firstChange && this.modelValue) {
this.onValidatorChange();
}
}
}
private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean {
const arg = obj?.schedule?.dynamicValueArgument;
return !arg || validArguments.includes(arg);
}
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
const filters = obj?.expression?.filters || obj?.filters || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
}
return true;
} }
public conditionSet() { public conditionSet() {
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
} }
public validate(): ValidationErrors | null { public validate(control: AbstractControl): ValidationErrors | null {
return this.conditionSet() ? null : { this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments));
this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments));
this.onValidatorChange = () => {
control.updateValueAndValidity({ emitEvent: true });
};
return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : {
alarmRuleCondition: { alarmRuleCondition: {
valid: false, valid: false,
} }
@ -226,6 +289,9 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
private updateModel() { private updateModel() {
this.updateConditionInfo(); this.updateConditionInfo();
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
if (this.modelValue) {
this.onValidatorChange();
}
} }
public openScheduleDialog($event: Event) { public openScheduleDialog($event: Event) {

6
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html

@ -20,11 +20,11 @@
</tb-cf-alarm-rule-condition> </tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" translate> <div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-additional-info-icon-hint' | translate }}" translate>
alarm-rule.alarm-rule-additional-info alarm-rule.alarm-rule-additional-info
</div> </div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="alarmDetails" placeholder="{{ 'alarm-rule.alarm-rule-additional-info' | translate }}"> <input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<button type="button" <button type="button"
matSuffix mat-icon-button aria-label="Open in new" matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)"> (click)="openEditDetailsDialog($event)">
@ -42,7 +42,7 @@
subscriptSizing="dynamic" subscriptSizing="dynamic"
inlineField inlineField
class="flex-1" class="flex-1"
placeholder="{{ 'alarm-rule.alarm-rule-mobile-dashboard' | translate }}" placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId"> formControlName="dashboardId">
</tb-dashboard-autocomplete> </tb-dashboard-autocomplete>
</div> </div>

5
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss

@ -20,6 +20,11 @@
display: grid; display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content; grid-template-rows: min-content minmax(auto, 1fr) min-content;
} }
.spec-text {
font-size: 14px;
color: rgba(0, 0, 0, 0.54);
}
} }
.tbel-script-lang-chip { .tbel-script-lang-chip {
line-height: 20px; line-height: 20px;

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts

@ -171,7 +171,7 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
if (value) { if (value) {
this.modelValue = value; this.modelValue = value;
if (this.modelValue.dynamicValueArgument) { if (this.modelValue.dynamicValueArgument) {
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false}); this.alarmScheduleForm.get('dynamicValueArgument').patchValue(Object.keys(this.arguments).includes(this.modelValue.dynamicValueArgument) ? this.modelValue.dynamicValueArgument : null, {emitEvent: false});
} else { } else {
switch (this.modelValue.staticValue.type) { switch (this.modelValue.staticValue.type) {
case AlarmRuleScheduleType.SPECIFIC_TIME: case AlarmRuleScheduleType.SPECIFIC_TIME:

33
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html

@ -25,20 +25,25 @@
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
<mat-form-field class="mat-block" appearance="outline"> <section class="tb-form-panel">
<mat-label translate>filter.operation.operation</mat-label> <div class="flex flex-row items-center justify-between">
<mat-select required formControlName="operation"> <div class="tb-form-panel-title"
<mat-option *ngFor="let operation of complexOperations" [value]="operation"> tb-hint-tooltip-icon="{{ data.valueType === EntityKeyValueType.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} {{ 'alarm-rule.filters' | translate }}
</mat-option> </div>
</mat-select> <tb-toggle-select formControlName="operation"
</mat-form-field> selectMediaBreakpoint="xs">
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType" <tb-toggle-option [value]="complexOperationEnum.AND">{{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }}</tb-toggle-option>
[arguments]="arguments" <tb-toggle-option [value]="complexOperationEnum.OR">{{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }}</tb-toggle-option>
[argumentInUse]="data.argumentInUse" </tb-toggle-select>
[operation]="complexFilterFormGroup.get('operation').value" </div>
formControlName="predicates"> <tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
</tb-alarm-rule-filter-predicate-list> [arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</section>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"

18
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts

@ -21,13 +21,13 @@ import { AppState } from '@core/core.state';
import { FormBuilder, Validators } from '@angular/forms'; import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { ComplexOperation, EntityKeyValueType } from '@shared/models/query/query.models';
import { import {
ComplexOperation, AlarmRuleFilterPredicate,
complexOperationTranslationMap, AlarmRuleFilterPredicateType,
EntityKeyValueType, ComplexAlarmRuleFilterPredicate,
FilterPredicateType filterOperationTranslationMap
} from '@shared/models/query/query.models'; } from "@shared/models/alarm-rule.models";
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export interface AlarmRuleComplexFilterPredicateDialogData { export interface AlarmRuleComplexFilterPredicateDialogData {
@ -55,9 +55,9 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
} }
); );
complexOperations = Object.keys(ComplexOperation); EntityKeyValueType = EntityKeyValueType;
complexOperationEnum = ComplexOperation; complexOperationEnum = ComplexOperation;
complexOperationTranslations = complexOperationTranslationMap; complexOperationTranslations = filterOperationTranslationMap;
isAdd: boolean; isAdd: boolean;
@ -81,7 +81,7 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
save(): void { save(): void {
const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate; const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate;
predicate.type = FilterPredicateType.COMPLEX; predicate.type = AlarmRuleFilterPredicateType.COMPLEX;
this.dialogRef.close(predicate); this.dialogRef.close(predicate);
} }
} }

4
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html

@ -69,7 +69,7 @@
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" <div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ filterFormGroup.get('valueType').value === entityKeyValueTypeEnum.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}"> tb-hint-tooltip-icon="{{ filterFormGroup.get('valueType').value === entityKeyValueTypeEnum.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filter' | translate }} {{ 'alarm-rule.filters' | translate }}
</div> </div>
<tb-toggle-select formControlName="operation" <tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs"> selectMediaBreakpoint="xs">
@ -95,7 +95,7 @@
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty"> [disabled]="filterFormGroup.invalid || !filterFormGroup.dirty || !predicatesValid">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }} {{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button> </button>
</div> </div>

53
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts

@ -21,18 +21,18 @@
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { import { ComplexOperation, EntityKeyValueType, entityKeyValueTypesMap } from '@shared/models/query/query.models';
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypesMap
} from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; import {
AlarmRuleFilter,
AlarmRuleFilterPredicate,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { FormControlsFrom } from "@shared/models/tenant.model"; import { FormControlsFrom } from "@shared/models/tenant.model";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { isDefinedAndNotNull } from "@core/utils";
export interface AlarmRuleFilterDialogData { export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter; filter: AlarmRuleFilter;
@ -57,7 +57,9 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
entityKeyValueTypes = entityKeyValueTypesMap; entityKeyValueTypes = entityKeyValueTypesMap;
complexOperationTranslationMap = complexOperationTranslationMap; complexOperationTranslationMap = filterOperationTranslationMap;
predicatesValid: boolean = false;
ComplexOperation = ComplexOperation; ComplexOperation = ComplexOperation;
@ -78,12 +80,20 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
this.filterFormGroup = this.fb.group( this.filterFormGroup = this.fb.group(
{ {
argument: [this.data.filter.argument, [Validators.required]], argument: [this.argumentsList.includes(this.data.filter.argument) ? this.data.filter.argument : '' , [Validators.required]],
valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]], valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]],
predicates: [this.data.filter.predicates, [Validators.required]], predicates: [this.data.filter.predicates, [Validators.required]],
operation: [this.data.filter.operation ?? ComplexOperation.AND] operation: [this.data.filter.operation ?? ComplexOperation.AND]
} }
); );
this.predicatesValid = this.isPredicateArgumentsValid(this.data.filter.predicates);
this.filterFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicatesValid = this.isPredicateArgumentsValid(predicates);
});
this.filterFormGroup.get('valueType').valueChanges.pipe( this.filterFormGroup.get('valueType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((valueType: EntityKeyValueType) => { ).subscribe((valueType: EntityKeyValueType) => {
@ -106,6 +116,31 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
}); });
} }
private isPredicateArgumentsValid(predicates: any): boolean {
const validSet = new Set(Object.keys(this.data.arguments));
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (isDefinedAndNotNull(p.value?.dynamicValueArgument)) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) {
return false;
}
}
return true;
}
argumentInUse(argument: string): boolean { argumentInUse(argument: string): boolean {
return this.data.usedArguments.includes(argument); return this.data.usedArguments.includes(argument);
} }

32
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html

@ -39,19 +39,26 @@
</div> </div>
<div class="flex max-w-92% flex-full flex-col"> <div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start"> <div class="flex flex-row items-center justify-start">
<div class="flex-1">{{ filterControl.value?.argument }}</div> <div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div>
<div class="flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}</div> <div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" <button mat-icon-button color="primary" class="tb-budge-button"
type="button" type="button"
(click)="editFilter(index)" (click)="editFilter(index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}" matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>{{'edit'}}</mat-icon> <mat-icon
[matBadgeHidden]="areFilterAndPredicateArgumentsValid(filterControl.value)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button> </button>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
type="button" type="button"
(click)="removeFilter(index)" (click)="removeFilter(index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}" matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
@ -62,14 +69,15 @@
} }
</div> </div>
} }
<span [class.!hidden]="!!filtersFormArray.length" @if (!filtersFormArray.length) {
class="no-data-found flex items-center justify-center" <span class="no-data-found tb-prompt flex items-center justify-center" translate>
translate> alarm-rule.no-filter
filter.no-key-filters </span>
</span> <tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div> </div>
</section> </section>
<button mat-button mat-raised-button color="primary" <button mat-button mat-stroked-button color="primary"
(click)="addFilter()" (click)="addFilter()"
type="button"> type="button">
{{ 'alarm-rule.add-filter' | translate }} {{ 'alarm-rule.add-filter' | translate }}

13
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss

@ -30,6 +30,10 @@
&-divider { &-divider {
border-top: 1px solid rgba(0, 0, 0, 0.12); border-top: 1px solid rgba(0, 0, 0, 0.12);
} }
.filters-text {
font-size: 14px;
}
} }
.filters-operation { .filters-operation {
display: flex; display: flex;
@ -39,7 +43,8 @@
background-color: white; background-color: white;
} }
&-label { &-label {
font-weight: 500; font-size: 15px;
font-weight: 400;
color: #00695C; color: #00695C;
padding: 0 8px; padding: 0 8px;
border-radius: 4px; border-radius: 4px;
@ -47,4 +52,10 @@
background-color: rgba(#00695C, 0.04); background-color: rgba(#00695C, 0.04);
} }
} }
.tb-budge-button {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0;
}
} }

30
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts

@ -102,6 +102,36 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
}; };
} }
public areFilterAndPredicateArgumentsValid(obj: any): boolean {
const validSet = new Set(Object.keys(this.arguments));
const filter = obj || [];
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
return true;
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
if (isDisabled) { if (isDisabled) {
this.filterListFormGroup.disable({emitEvent: false}); this.filterListFormGroup.disable({emitEvent: false});

16
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html

@ -58,21 +58,23 @@
</div> </div>
</div> </div>
} }
<span [class.!hidden]="!!predicatesFormArray.length" @if (!predicatesFormArray.length) {
[class.disabled]="disabled" <span class="no-data-found tb-prompt flex items-center justify-center" translate>
class="no-data-found flex items-center justify-center" translate> alarm-rule.no-filter
filter.no-filters </span>
</span> <tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div> </div>
</div> </div>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button mat-button mat-raised-button color="primary" <button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled" [class.!hidden]="disabled"
(click)="addPredicate(false)" (click)="addPredicate(false)"
type="button"> type="button">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
<button mat-button mat-raised-button color="primary" <button mat-button
color="primary"
[class.!hidden]="disabled" [class.!hidden]="disabled"
(click)="addPredicate(true)" (click)="addPredicate(true)"
type="button"> type="button">

3
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss

@ -39,7 +39,8 @@
background-color: white; background-color: white;
} }
&-label { &-label {
font-weight: 500; font-size: 15px;
font-weight: 400;
color: #00695C; color: #00695C;
padding: 0 8px; padding: 0 8px;
border-radius: 4px; border-radius: 4px;

26
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts

@ -28,14 +28,10 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { import {
BooleanOperation,
ComplexOperation, ComplexOperation,
complexOperationTranslationMap, complexOperationTranslationMap,
EntityKeyValueType, EntityKeyValueType,
entityKeyValueTypeToFilterPredicateType, entityKeyValueTypeToFilterPredicateType
FilterPredicateType,
NumericOperation,
StringOperation
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -44,8 +40,12 @@ import {
AlarmRuleComplexFilterPredicateDialogData AlarmRuleComplexFilterPredicateDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; } from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
import { import {
AlarmRuleBooleanOperation,
AlarmRuleFilterPredicate, AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
AlarmRuleNumericOperation,
AlarmRulePredicateInfo, AlarmRulePredicateInfo,
AlarmRuleStringOperation,
ComplexAlarmRuleFilterPredicate ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
@ -157,29 +157,29 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate { private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate {
const predicate = { const predicate = {
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType) type: complex ? AlarmRuleFilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
} as AlarmRuleFilterPredicate; } as AlarmRuleFilterPredicate;
switch (predicate.type) { switch (predicate.type) {
case FilterPredicateType.STRING: case AlarmRuleFilterPredicateType.STRING:
predicate.operation = StringOperation.STARTS_WITH; predicate.operation = AlarmRuleStringOperation.STARTS_WITH;
predicate.value = { predicate.value = {
staticValue: '' staticValue: ''
}; };
predicate.ignoreCase = false; predicate.ignoreCase = false;
break; break;
case FilterPredicateType.NUMERIC: case AlarmRuleFilterPredicateType.NUMERIC:
predicate.operation = NumericOperation.EQUAL; predicate.operation = AlarmRuleNumericOperation.EQUAL;
predicate.value = { predicate.value = {
staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0 staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0
}; };
break; break;
case FilterPredicateType.BOOLEAN: case AlarmRuleFilterPredicateType.BOOLEAN:
predicate.operation = BooleanOperation.EQUAL; predicate.operation = AlarmRuleBooleanOperation.EQUAL;
predicate.value = { predicate.value = {
staticValue: false staticValue: false
}; };
break; break;
case FilterPredicateType.COMPLEX: case AlarmRuleFilterPredicateType.COMPLEX:
predicate.operation = ComplexOperation.AND; predicate.operation = ComplexOperation.AND;
predicate.predicates = []; predicate.predicates = [];
break; break;

77
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html

@ -0,0 +1,77 @@
<!--
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.
-->
<div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueNoDataFormGroup">
<div class="tb-form-row no-padding no-border column-xs">
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option>
<mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option>
</mat-select>
</mat-form-field>
<ng-container formGroupName="duration">
@if (!dynamicModeControl.value) {
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input required type="number" matInput formControlName="staticValue" min="1"
placeholder="{{'filter.value' | translate}}">
@if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.staticValue').hasError('required') ? 'alarm-rule.value-required' : 'alarm-rule.min-value') | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
} @else {
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option>
}
</mat-select>
@if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').touched && filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').errors) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').hasError('required') ? 'calculated-fields.hint.argument-name-required' : 'alarm-rule.argument-in-use') | translate"
class="tb-error !block">
warning
</mat-icon>
}
@if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').hasError('argumentInUse')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'alarm-rule.argument-in-use' | translate"
class="tb-error !block">
warning
</mat-icon>
}
</mat-form-field>
}
</ng-container>
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="unit" required placeholder="{{'alarm-rule.time-unit' | translate}}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitsTranslationMap.get(timeUnit) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>

166
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts

@ -0,0 +1,166 @@
///
/// 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.
///
import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { TimeUnit, timeUnitTranslations } from "@home/components/rule-node/rule-node-config.models";
import { AlarmRuleFilterPredicateType, NoDataAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { isDefinedAndNotNull } from "@core/utils";
@Component({
selector: 'tb-alarm-rule-filter-predicate-no-data-value',
templateUrl: './alarm-rule-filter-predicate-no-data-value.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent),
multi: true
}
]
})
export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
valueType: AlarmRuleFilterPredicateType;
@Input()
argumentInUse: string;
valueTypeEnum = AlarmRuleFilterPredicateType;
filterPredicateValueNoDataFormGroup = this.fb.group({
type: ['NO_DATA'],
unit: [TimeUnit.MINUTES, Validators.required],
duration: this.fb.group({
staticValue: [null as null | number, [Validators.required, Validators.min(1)]],
dynamicValueArgument: ['', Validators.required]
})
});
timeUnits = [TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS];
timeUnitsTranslationMap = timeUnitTranslations;
dynamicModeControl = this.fb.control(false);
argumentsList: Array<string>;
private propagateChange= (v: any) => { };
constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
this.filterPredicateValueNoDataFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
this.dynamicModeControl.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => this.updateValueModeValidators(value));
}
ngOnChanges(changes: SimpleChanges) {
if (changes.argumentInUse) {
const argumentInUseChanges = changes.argumentInUse;
if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) {
if (this.dynamicModeControl.value) {
if (this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value) {
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueNoDataFormGroup.updateValueAndValidity();
}
}
}
}
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.filterPredicateValueNoDataFormGroup.disable({emitEvent: false});
this.dynamicModeControl.disable({emitEvent: false});
} else {
this.filterPredicateValueNoDataFormGroup.enable({emitEvent: false});
this.dynamicModeControl.enable({emitEvent: false});
this.updateValueModeValidators(this.dynamicModeControl.value);
}
}
private updateValueModeValidators(isDynamicMode: boolean): void {
if (isDynamicMode) {
this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').disable({emitEvent: false});
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').enable();
setTimeout(()=> {
if (this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('dynamicValueArgument').value) {
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueNoDataFormGroup.updateValueAndValidity();
}
}, 0);
} else {
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').disable({emitEvent: false});
this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').enable();
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
validate(): ValidationErrors | null {
return this.filterPredicateValueNoDataFormGroup.valid ? null : {
filterPredicateValue: {valid: false}
};
}
writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void {
if (isDefinedAndNotNull(predicateValue.duration.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) {
predicateValue.duration.dynamicValueArgument = '';
}
this.dynamicModeControl.patchValue(true, {emitEvent: false});
}
this.filterPredicateValueNoDataFormGroup.patchValue(predicateValue, {emitEvent: false});
}
private updateModel() {
this.propagateChange(this.filterPredicateValueNoDataFormGroup.value);
}
}

9
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html

@ -67,6 +67,15 @@
warning warning
</mat-icon> </mat-icon>
} }
@if (filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('argumentInUse')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'alarm-rule.argument-in-use' | translate"
class="tb-error !block">
warning
</mat-icon>
}
</mat-form-field> </mat-form-field>
} }
</div> </div>

33
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -31,6 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmRuleValue } from "@shared/models/alarm-rule.models"; import { AlarmRuleValue } from "@shared/models/alarm-rule.models";
import { FormControlsFrom } from "@shared/models/tenant.model"; import { FormControlsFrom } from "@shared/models/tenant.model";
import { isDefinedAndNotNull } from "@core/utils";
@Component({ @Component({
selector: 'tb-alarm-rule-filter-predicate-value', selector: 'tb-alarm-rule-filter-predicate-value',
@ -49,7 +50,7 @@ import { FormControlsFrom } from "@shared/models/tenant.model";
} }
] ]
}) })
export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
@ -110,6 +111,20 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
).subscribe(value => this.updateValueModeValidators(value)); ).subscribe(value => this.updateValueModeValidators(value));
} }
ngOnChanges(changes: SimpleChanges) {
if (changes.argumentInUse) {
const argumentInUseChanges = changes.argumentInUse;
if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) {
if (this.dynamicModeControl.value) {
if (this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) {
this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueFormGroup.updateValueAndValidity();
}
}
}
}
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
if (isDisabled) { if (isDisabled) {
this.filterPredicateValueFormGroup.disable({emitEvent: false}); this.filterPredicateValueFormGroup.disable({emitEvent: false});
@ -125,6 +140,12 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
if (isDynamicMode) { if (isDynamicMode) {
this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable(); this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable();
setTimeout(()=> {
if (this.filterPredicateValueFormGroup.get('dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) {
this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true});
this.filterPredicateValueFormGroup.updateValueAndValidity();
}
}, 0);
} else { } else {
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false}); this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('staticValue').enable(); this.filterPredicateValueFormGroup.get('staticValue').enable();
@ -145,8 +166,14 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
} }
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void { writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
if (isDefinedAndNotNull(predicateValue.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.dynamicValueArgument)) {
predicateValue.dynamicValueArgument = '';
}
this.dynamicModeControl.patchValue(true, {emitEvent: false});
}
this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false});
this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false});
} }
private updateModel() { private updateModel() {

19
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html

@ -27,9 +27,11 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true"> @if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) {
<mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option> <mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true">
</mat-chip-listbox> <mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option>
</mat-chip-listbox>
}
</div> </div>
} }
@case (filterPredicateType.NUMERIC) { @case (filterPredicateType.NUMERIC) {
@ -58,7 +60,7 @@
<div class="tb-form-row no-border no-padding flex-1"> <div class="tb-form-row no-border no-padding flex-1">
<button type="button" style="--mat-outlined-button-horizontal-padding: 3px 0px 12px;" <button type="button" style="--mat-outlined-button-horizontal-padding: 3px 0px 12px;"
class="block w-full" class="block w-full"
mat-stroked-button color="primary" mat-stroked-button [color]="predicateValid ? 'primary' : 'warn'"
(click)="openComplexFilterDialog()"> (click)="openComplexFilterDialog()">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-full text-start" translate>filter.complex-filter</span> <span class="w-full text-start" translate>filter.complex-filter</span>
@ -68,7 +70,14 @@
</div> </div>
} }
} }
@if (type !== filterPredicateType.COMPLEX) { @if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) {
<tb-alarm-rule-filter-predicate-no-data-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="duration">
</tb-alarm-rule-filter-predicate-no-data-value>
} @else if (type !== filterPredicateType.COMPLEX) {
<tb-alarm-rule-filter-predicate-value class="flex-full" <tb-alarm-rule-filter-predicate-value class="flex-full"
[arguments]="arguments" [arguments]="arguments"
[argumentInUse]="argumentInUse" [argumentInUse]="argumentInUse"

122
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts

@ -23,18 +23,19 @@ import {
ValidationErrors, ValidationErrors,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
import { import { EntityKeyValueType } from '@shared/models/query/query.models';
BooleanOperation,
booleanOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
numericOperationTranslationMap,
StringOperation,
stringOperationTranslationMap
} from '@shared/models/query/query.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; import {
AlarmRuleBooleanOperation,
alarmRuleBooleanOperationTranslationMap,
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
AlarmRuleNumericOperation,
alarmRuleNumericOperationTranslationMap,
AlarmRuleStringOperation,
alarmRuleStringOperationTranslationMap,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { import {
AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogComponent,
@ -77,24 +78,27 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
operation: [], operation: [],
ignoreCase: false, ignoreCase: false,
predicates: [], predicates: [],
value: [] value: [],
duration: []
}); });
type: FilterPredicateType; type: AlarmRuleFilterPredicateType;
filterPredicateType = AlarmRuleFilterPredicateType;
filterPredicateType = FilterPredicateType; stringOperations = Object.keys(AlarmRuleStringOperation);
stringOperation = AlarmRuleStringOperation;
stringOperationTranslationMap = alarmRuleStringOperationTranslationMap;
stringOperations = Object.keys(StringOperation); numericOperations = Object.keys(AlarmRuleNumericOperation);
stringOperation = StringOperation; numericOperationEnum = AlarmRuleNumericOperation;
stringOperationTranslationMap = stringOperationTranslationMap; numericOperationTranslations = alarmRuleNumericOperationTranslationMap;
numericOperations = Object.keys(NumericOperation); booleanOperations = Object.keys(AlarmRuleBooleanOperation);
numericOperationEnum = NumericOperation; booleanOperationEnum = AlarmRuleBooleanOperation;
numericOperationTranslations = numericOperationTranslationMap; booleanOperationTranslations = alarmRuleBooleanOperationTranslationMap;
booleanOperations = Object.keys(BooleanOperation); predicateValid: boolean = false;
booleanOperationEnum = BooleanOperation;
booleanOperationTranslations = booleanOperationTranslationMap;
private propagateChange= (v: any) => { }; private propagateChange= (v: any) => { };
@ -106,6 +110,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
).subscribe(() => { ).subscribe(() => {
this.updateModel(); this.updateModel();
}); });
this.filterPredicateFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicateValid = this.isPredicateArgumentsValid(predicates);
})
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -130,13 +140,75 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
} }
} }
private isPredicateArgumentsValid(predicates: any): boolean {
const validSet = new Set(Object.keys(this.arguments));
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) {
return false;
}
}
return true;
}
writeValue(predicate: AlarmRuleFilterPredicate): void { writeValue(predicate: AlarmRuleFilterPredicate): void {
this.type = predicate.type; this.type = predicate.type;
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); if ((predicate as ComplexAlarmRuleFilterPredicate)?.predicates) {
this.predicateValid = this.isPredicateArgumentsValid((predicate as ComplexAlarmRuleFilterPredicate)?.predicates);
}
if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) {
this.type = AlarmRuleFilterPredicateType[this.valueType];
this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false});
} else {
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
}
} }
private updateModel() { private updateModel() {
this.propagateChange({type: this.type, ...this.filterPredicateFormGroup.value}); const predicate = this.filterPredicateFormGroup.value;
if (predicate.operation === 'NO_DATA') {
this.propagateChange(predicate.duration);
} else {
if (!predicate.value) {
switch (this.valueType) {
case EntityKeyValueType.STRING:
predicate.value = {
staticValue: ''
};
break;
case EntityKeyValueType.NUMERIC:
predicate.value = {
staticValue: 0
};
break;
case EntityKeyValueType.DATE_TIME:
predicate.value = {
staticValue: Date.now()
};
break;
case EntityKeyValueType.BOOLEAN:
predicate.value = {
staticValue: false
};
break;
}
}
this.propagateChange({type: this.type, ...predicate});
}
} }
public openComplexFilterDialog() { public openComplexFilterDialog() {

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html

@ -19,5 +19,6 @@
tbTruncateWithTooltip tbTruncateWithTooltip
[class.required]="isRequired" [class.required]="isRequired"
[class.nowrap]="nowrap" [class.nowrap]="nowrap"
[class.wrap]="!nowrap"
[innerHTML]="filterText"> [innerHTML]="filterText">
</div> </div>

3
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss

@ -28,6 +28,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
&.wrap {
white-space: pre-wrap !important;
}
} }
} }

36
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts

@ -16,25 +16,27 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { import {
booleanOperationTranslationMap,
ComplexOperation, ComplexOperation,
complexOperationTranslationMap, complexOperationTranslationMap,
EntityKeyValueType, EntityKeyValueType
FilterPredicateType,
numericOperationTranslationMap,
stringOperationTranslationMap
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { import {
alarmRuleBooleanOperationTranslationMap,
AlarmRuleExpression, AlarmRuleExpression,
AlarmRuleExpressionType, AlarmRuleExpressionType,
AlarmRuleFilter, AlarmRuleFilter,
AlarmRuleFilterPredicate, AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
alarmRuleNumericOperationTranslationMap,
AlarmRuleStringOperation,
alarmRuleStringOperationTranslationMap,
ComplexAlarmRuleFilterPredicate ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { coerceBoolean } from "@shared/decorators/coercion"; import { coerceBoolean } from "@shared/decorators/coercion";
import { timeUnitTranslationMap } from "@shared/models/time/time.models";
@Component({ @Component({
selector: 'tb-alarm-rule-filter-text', selector: 'tb-alarm-rule-filter-text',
@ -96,7 +98,7 @@ export class AlarmRuleFilterTextComponent {
private updateFilterText(value: AlarmRuleExpression) { private updateFilterText(value: AlarmRuleExpression) {
this.isRequired = false; this.isRequired = false;
if (value && (value.expression || value.filters)) { if (value && (value.expression || value.filters?.length)) {
if (value.type === AlarmRuleExpressionType.SIMPLE) { if (value.type === AlarmRuleExpressionType.SIMPLE) {
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation); this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
} else { } else {
@ -137,21 +139,21 @@ export class AlarmRuleFilterTextComponent {
const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND); const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND);
const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => { const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => {
if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { if (keyFilterPredicate.type === AlarmRuleFilterPredicateType.COMPLEX) {
const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate; const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate;
const complexOperation = complexPredicate.operation ?? ComplexOperation.AND; const complexOperation = complexPredicate.operation ?? ComplexOperation.AND;
return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation); return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation);
} else { } else {
let operation: string; let operation: string;
let value: string; let value: string;
const val = keyFilterPredicate.value; const val = keyFilterPredicate.type === AlarmRuleFilterPredicateType.NO_DATA ? keyFilterPredicate.duration : keyFilterPredicate.value;
const dynamicValue = val?.dynamicValueArgument?.length; const dynamicValue = val?.dynamicValueArgument?.length;
if (dynamicValue) { if (dynamicValue) {
value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-value">' + val?.dynamicValueArgument + '</span></span>'; value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-value">' + val?.dynamicValueArgument + '</span></span>';
} }
switch (keyFilterPredicate.type) { switch (keyFilterPredicate.type) {
case FilterPredicateType.STRING: case AlarmRuleFilterPredicateType.STRING:
operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); operation = translate.instant(alarmRuleStringOperationTranslationMap.get(keyFilterPredicate.operation));
if (keyFilterPredicate.ignoreCase) { if (keyFilterPredicate.ignoreCase) {
operation += ' ' + translate.instant('filter.ignore-case'); operation += ' ' + translate.instant('filter.ignore-case');
} }
@ -159,8 +161,8 @@ export class AlarmRuleFilterTextComponent {
value = `'${keyFilterPredicate.value.staticValue}'`; value = `'${keyFilterPredicate.value.staticValue}'`;
} }
break; break;
case FilterPredicateType.NUMERIC: case AlarmRuleFilterPredicateType.NUMERIC:
operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); operation = translate.instant(alarmRuleNumericOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) { if (!dynamicValue) {
if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) {
value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm'); value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm');
@ -169,12 +171,18 @@ export class AlarmRuleFilterTextComponent {
} }
} }
break; break;
case FilterPredicateType.BOOLEAN: case AlarmRuleFilterPredicateType.BOOLEAN:
operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); operation = translate.instant(alarmRuleBooleanOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) { if (!dynamicValue) {
value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false'); value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false');
} }
break; break;
case AlarmRuleFilterPredicateType.NO_DATA:
operation = translate.instant(alarmRuleStringOperationTranslationMap.get(AlarmRuleStringOperation.NO_DATA));
if (!dynamicValue) {
value = keyFilterPredicate.duration.staticValue + ' ' + translate.instant(timeUnitTranslationMap.get(keyFilterPredicate.unit)).toLowerCase();
}
break;
} }
if (!dynamicValue) { if (!dynamicValue) {
value = `<span class="tb-filter-value">${value}</span>`; value = `<span class="tb-filter-value">${value}</span>`;

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

@ -64,6 +64,7 @@ export class EntityAggregationArgumentsTableComponent extends CalculatedFieldArg
hint: 'calculated-fields.entity-aggregation.argument-setting-hint', hint: 'calculated-fields.entity-aggregation.argument-setting-hint',
hiddenDefaultValue: true, hiddenDefaultValue: true,
hiddenEntityKeyTypes: true, hiddenEntityKeyTypes: true,
watchKeyChange: true,
}; };
this.isScript = false; this.isScript = false;

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

@ -123,7 +123,7 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa
this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
} else if (!this.argumentsFormArray.controls.length) { } else if (!this.argumentsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.arguments-empty'; this.errorText = 'calculated-fields.hint.arguments-empty';
} if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) { } else if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) {
this.errorText = 'calculated-fields.hint.arguments-propagate-argument-must-current-entity'; this.errorText = 'calculated-fields.hint.arguments-propagate-argument-must-current-entity';
} else { } else {
this.errorText = ''; this.errorText = '';

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

@ -87,7 +87,8 @@ export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldAr
hiddenEntityTypes: true, hiddenEntityTypes: true,
defaultValueRequired: true, defaultValueRequired: true,
argumentEntityTypes: [ArgumentEntityType.Current], argumentEntityTypes: [ArgumentEntityType.Current],
hint: 'calculated-fields.hint.setting-arguments-aggregation' hint: 'calculated-fields.hint.setting-arguments-aggregation',
watchKeyChange: true,
}; };
this.isScript = false; this.isScript = false;

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html

@ -59,7 +59,7 @@
</div> </div>
<div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"> <div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value">
<div class="tb-form-panel-title tb-required"> <div class="tb-form-panel-title tb-required">
{{ 'calculated-fields.expression' | translate }} {{ 'calculated-fields.script' | translate }}
</div> </div>
<div> <div>
<tb-js-func required <tb-js-func required

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -145,7 +145,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
const formValue: any = deepClone(value); const formValue: any = deepClone(value);
if (this.isScript) { if (this.isScript) {
formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript; formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript;
} else { } else if (value.type === CalculatedFieldType.SIMPLE) {
formValue.expressionSIMPLE = formValue.expression; formValue.expressionSIMPLE = formValue.expression;
} }
this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); this.simpleConfiguration.patchValue(formValue, {emitEvent: false});

4
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html

@ -74,8 +74,8 @@
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations"> <mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }} {{ 'version-control.export-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields"> <mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields" class="checkbox-pre-line">
{{ 'version-control.export-calculated-fields' | translate }} {{ (entityTypeFormGroup.get('entityType').value === entityTypes.CUSTOMER ? 'version-control.export-alarm-rules' : 'version-control.export-calculated-fields') | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>

11
ui-ngx/src/app/modules/home/components/vc/entity-types-version.component.scss

@ -56,6 +56,17 @@
} }
:host ::ng-deep { :host ::ng-deep {
.mat-expansion-panel.entity-type-config {
.entity-type-config-content {
.checkbox-pre-line {
.mdc-form-field {
.mdc-label {
white-space: pre-line;
}
}
}
}
}
.mat-expansion-panel.entity-type-config { .mat-expansion-panel.entity-type-config {
.mat-expansion-panel-body { .mat-expansion-panel-body {
padding: 0; padding: 0;

2
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -43,7 +43,7 @@ const routes: Routes = [
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
breadcrumb: { breadcrumb: {
menuId: MenuId.alarms menuId: MenuId.alarms_center
} }
}, },
children: [ children: [

101
ui-ngx/src/app/shared/models/alarm-rule.models.ts

@ -120,7 +120,8 @@ export interface AlarmRulePredicateInfo {
export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate | export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate |
NumericAlarmRuleFilterPredicate | NumericAlarmRuleFilterPredicate |
BooleanAlarmRuleFilterPredicate | BooleanAlarmRuleFilterPredicate |
ComplexAlarmRuleFilterPredicate; ComplexAlarmRuleFilterPredicate |
NoDataAlarmRuleFilterPredicate;
export interface AlarmRuleValue<T> { export interface AlarmRuleValue<T> {
dynamicValueArgument?: string; dynamicValueArgument?: string;
@ -128,26 +129,33 @@ export interface AlarmRuleValue<T> {
} }
export interface StringAlarmRuleFilterPredicate { export interface StringAlarmRuleFilterPredicate {
type: FilterPredicateType.STRING; type: AlarmRuleFilterPredicateType.STRING;
operation: StringOperation; operation: AlarmRuleStringOperation;
value: AlarmRuleValue<string>; value: AlarmRuleValue<string>;
ignoreCase: boolean; ignoreCase: boolean;
} }
export interface NumericAlarmRuleFilterPredicate { export interface NumericAlarmRuleFilterPredicate {
type: FilterPredicateType.NUMERIC; type: AlarmRuleFilterPredicateType.NUMERIC;
operation: NumericOperation; operation: AlarmRuleNumericOperation;
value: AlarmRuleValue<number>; value: AlarmRuleValue<number>;
} }
export interface BooleanAlarmRuleFilterPredicate { export interface BooleanAlarmRuleFilterPredicate {
type: FilterPredicateType.BOOLEAN; type: AlarmRuleFilterPredicateType.BOOLEAN;
operation: BooleanOperation; operation: AlarmRuleBooleanOperation;
value: AlarmRuleValue<boolean>; value: AlarmRuleValue<boolean>;
} }
export interface NoDataAlarmRuleFilterPredicate {
type: AlarmRuleFilterPredicateType.NO_DATA;
unit: TimeUnit,
operation: AlarmRuleStringOperation.NO_DATA | AlarmRuleNumericOperation.NO_DATA | AlarmRuleBooleanOperation.NO_DATA;
duration: AlarmRuleValue<number>;
}
export interface BaseComplexFilterPredicate<T extends AlarmRuleFilterPredicate> { export interface BaseComplexFilterPredicate<T extends AlarmRuleFilterPredicate> {
type: FilterPredicateType.COMPLEX; type: AlarmRuleFilterPredicateType.COMPLEX;
operation: ComplexOperation; operation: ComplexOperation;
predicates: Array<T>; predicates: Array<T>;
} }
@ -160,6 +168,83 @@ export interface AlarmRuleFilterConfig {
entities?: Array<string>; entities?: Array<string>;
} }
export const filterOperationTranslationMap = new Map<ComplexOperation, string>(
[
[ComplexOperation.AND, 'alarm-rule.filter-operation.and'],
[ComplexOperation.OR, 'alarm-rule.filter-operation.or'],
]
);
export enum AlarmRuleFilterPredicateType {
STRING = 'STRING',
NUMERIC = 'NUMERIC',
BOOLEAN = 'BOOLEAN',
COMPLEX = 'COMPLEX',
NO_DATA = 'NO_DATA'
}
export enum AlarmRuleStringOperation {
EQUAL = 'EQUAL',
NOT_EQUAL = 'NOT_EQUAL',
NO_DATA = 'NO_DATA',
STARTS_WITH = 'STARTS_WITH',
ENDS_WITH = 'ENDS_WITH',
CONTAINS = 'CONTAINS',
NOT_CONTAINS = 'NOT_CONTAINS',
IN = 'IN',
NOT_IN = 'NOT_IN',
}
export const alarmRuleStringOperationTranslationMap = new Map<AlarmRuleStringOperation, string>(
[
[AlarmRuleStringOperation.EQUAL, 'filter.operation.equal'],
[AlarmRuleStringOperation.NOT_EQUAL, 'filter.operation.not-equal'],
[AlarmRuleStringOperation.STARTS_WITH, 'filter.operation.starts-with'],
[AlarmRuleStringOperation.ENDS_WITH, 'filter.operation.ends-with'],
[AlarmRuleStringOperation.CONTAINS, 'filter.operation.contains'],
[AlarmRuleStringOperation.NOT_CONTAINS, 'filter.operation.not-contains'],
[AlarmRuleStringOperation.IN, 'filter.operation.in'],
[AlarmRuleStringOperation.NOT_IN, 'filter.operation.not-in'],
[AlarmRuleStringOperation.NO_DATA, 'alarm-rule.missing-for']
]
);
export enum AlarmRuleNumericOperation {
EQUAL = 'EQUAL',
NOT_EQUAL = 'NOT_EQUAL',
NO_DATA = 'NO_DATA',
GREATER = 'GREATER',
LESS = 'LESS',
GREATER_OR_EQUAL = 'GREATER_OR_EQUAL',
LESS_OR_EQUAL = 'LESS_OR_EQUAL'
}
export const alarmRuleNumericOperationTranslationMap = new Map<AlarmRuleNumericOperation, string>(
[
[AlarmRuleNumericOperation.EQUAL, 'filter.operation.equal'],
[AlarmRuleNumericOperation.NOT_EQUAL, 'filter.operation.not-equal'],
[AlarmRuleNumericOperation.GREATER, 'filter.operation.greater'],
[AlarmRuleNumericOperation.LESS, 'filter.operation.less'],
[AlarmRuleNumericOperation.GREATER_OR_EQUAL, 'filter.operation.greater-or-equal'],
[AlarmRuleNumericOperation.LESS_OR_EQUAL, 'filter.operation.less-or-equal'],
[AlarmRuleNumericOperation.NO_DATA, 'alarm-rule.missing-for']
]
);
export enum AlarmRuleBooleanOperation {
EQUAL = 'EQUAL',
NOT_EQUAL = 'NOT_EQUAL',
NO_DATA = 'NO_DATA',
}
export const alarmRuleBooleanOperationTranslationMap = new Map<AlarmRuleBooleanOperation, string>(
[
[AlarmRuleBooleanOperation.EQUAL, 'filter.operation.equal'],
[AlarmRuleBooleanOperation.NOT_EQUAL, 'filter.operation.not-equal'],
[AlarmRuleBooleanOperation.NO_DATA, 'alarm-rule.missing-for']
]
);
export const alarmRuleDefaultScript = export const alarmRuleDefaultScript =
'// Sample expression for an alarm rule: triggers when temperature is above 20 degree\n' + '// Sample expression for an alarm rule: triggers when temperature is above 20 degree\n' +
'return temperature > 20;' 'return temperature > 20;'

2
ui-ngx/src/app/shared/models/vc.models.ts

@ -265,4 +265,4 @@ export interface EntityDataInfo {
hasCalculatedFields: boolean; hasCalculatedFields: boolean;
} }
export const typesWithCalculatedFields = new Set<EntityType | AliasEntityType>([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE]); export const typesWithCalculatedFields = new Set<EntityType | AliasEntityType>([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE, EntityType.CUSTOMER]);

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

@ -558,6 +558,7 @@
}, },
"alarm": { "alarm": {
"alarm": "Alarm", "alarm": "Alarm",
"alarm-list": "Alarm list",
"alarms": "Alarms", "alarms": "Alarms",
"all-alarms": "All alarms", "all-alarms": "All alarms",
"select-alarm": "Select alarm", "select-alarm": "Select alarm",
@ -1209,8 +1210,9 @@
"propagation-path-related-entities": "Propagation path to related entities", "propagation-path-related-entities": "Propagation path to related entities",
"propagate-type": { "propagate-type": {
"arguments-only": "Arguments only", "arguments-only": "Arguments only",
"expression-result": "Expression result" "expression-result": "Calculation result"
}, },
"script": "Script",
"data-propagate": "Data to propagate", "data-propagate": "Data to propagate",
"output-key": "Output key", "output-key": "Output key",
"copy-output-key": "Copy output key", "copy-output-key": "Copy output key",
@ -1368,7 +1370,7 @@
"arguments-aggregation": "Defines input parameters used for filtering and aggregation.", "arguments-aggregation": "Defines input parameters used for filtering and aggregation.",
"setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.", "setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.",
"metrics": "Defines metrics aggregated based on the configured arguments.", "metrics": "Defines metrics aggregated based on the configured arguments.",
"import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field type." "import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field structure."
} }
}, },
"alarm-rule": { "alarm-rule": {
@ -1378,7 +1380,7 @@
"alarm-rules-old": "Old", "alarm-rules-old": "Old",
"alarm-rules-actual": "Actual", "alarm-rules-actual": "Actual",
"severities": "Severities", "severities": "Severities",
"cleared": "Clear condition", "cleared": "Clearing condition",
"delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?", "delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?",
"delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.", "delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.",
"delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?", "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?",
@ -1395,7 +1397,8 @@
"entity-type": "Entity type", "entity-type": "Entity type",
"target-entity-type": "Target entity type", "target-entity-type": "Target entity type",
"entity-type-required": "Entity type is required.", "entity-type-required": "Entity type is required.",
"entity-name": "Entity name", "target-entity": "Target entity",
"target-entities": "Target entities",
"alarm-type": "Alarm type", "alarm-type": "Alarm type",
"alarm-type-required": "Alarm type is required.", "alarm-type-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.", "alarm-type-pattern": "Alarm type is invalid.",
@ -1412,7 +1415,7 @@
"condition-during": "During {{during}}", "condition-during": "During {{during}}",
"condition-during-dynamic": "During \"{{ attribute }}\"", "condition-during-dynamic": "During \"{{ attribute }}\"",
"condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }",
"condition-repeat-times-dynamic": "Repeats \"{ attribute }\"", "condition-repeat-times-dynamic": "Repeats \"{{ attribute }}\"",
"filter-preview": "Filter preview", "filter-preview": "Filter preview",
"condition-settings": "Condition settings", "condition-settings": "Condition settings",
"static": "Static", "static": "Static",
@ -1421,7 +1424,7 @@
"argument-name": "Argument name", "argument-name": "Argument name",
"value-type": "Value type", "value-type": "Value type",
"general": "General", "general": "General",
"filter": "Filter", "filters": "Filters",
"date-time-hint": "The argument must be in epoch milliseconds. Example: 1698839340000 equals 2023-11-01 12:49:00 UTC.", "date-time-hint": "The argument must be in epoch milliseconds. Example: 1698839340000 equals 2023-11-01 12:49:00 UTC.",
"operation": "Operation", "operation": "Operation",
"value-source": "Value source", "value-source": "Value source",
@ -1429,8 +1432,11 @@
"ignore-case": "Ignore case", "ignore-case": "Ignore case",
"condition": "Condition", "condition": "Condition",
"script": "Script", "script": "Script",
"add-filter": "Add filter", "add-filter": "Add argument filter",
"edit-filter": "Edit filter", "edit-filter": "Argument filter",
"remove-filter": "Remove argument filter",
"no-filter": "No argument filters configured",
"filter-required": "At least one filter must be configured.",
"conditions": { "conditions": {
"simple": "Simple", "simple": "Simple",
"duration": "Duration", "duration": "Duration",
@ -1478,16 +1484,18 @@
"edit-alarm-rule-additional-info": "Edit additional info", "edit-alarm-rule-additional-info": "Edit additional info",
"alarm-rule-additional-info-placeholder": "Please provide your comments and adjustments here to display them within Alarm details under Additional info", "alarm-rule-additional-info-placeholder": "Please provide your comments and adjustments here to display them within Alarm details under Additional info",
"alarm-rule-additional-info-hint": "Hint: use <code><span style=\"color: #000;\">$&#123;</span>Argument name<span style=\"color: #000;\">&#125;</span></code> to substitute values of the arguments that are used in alarm rule condition.", "alarm-rule-additional-info-hint": "Hint: use <code><span style=\"color: #000;\">$&#123;</span>Argument name<span style=\"color: #000;\">&#125;</span></code> to substitute values of the arguments that are used in alarm rule condition.",
"alarm-rule-additional-info-icon-hint": "Use Argument name to substitute values of the arguments that are used in alarm rule condition.",
"alarm-rule-mobile-dashboard": "Mobile dashboard", "alarm-rule-mobile-dashboard": "Mobile dashboard",
"alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard", "alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard",
"alarm-rule-no-mobile-dashboard": "No dashboard selected", "alarm-rule-no-mobile-dashboard": "No dashboard selected",
"alarm-rule-condition": "Alarm rule condition", "alarm-rule-condition": "Alarm rule condition",
"enter-alarm-rule-condition-prompt": "Add alarm rule creating condition", "enter-alarm-rule-condition-prompt": "Add condition",
"enter-alarm-rule-clear-condition-prompt": "Add alarm rule clearing condition", "enter-alarm-rule-clear-condition-prompt": "Add clearing condition",
"edit-alarm-rule-condition": "Edit alarm rule condition", "edit-alarm-rule-condition": "Alarm condition",
"condition-type": "Condition type", "condition-type": "Condition type",
"condition-type-hint": "\"Duration\" and \"Repeating\" options are not available when the \"Missing for\" operation is used in the filter.",
"select-alarm-severity": "Select alarm severity", "select-alarm-severity": "Select alarm severity",
"add-create-alarm-rule-prompt": "At least one creation condition should be specified", "add-create-alarm-rule-prompt": "At least one creation condition should be configured",
"add-create-alarm-rule": "Add creation condition", "add-create-alarm-rule": "Add creation condition",
"add-clear-alarm-rule": "Add clearing condition", "add-clear-alarm-rule": "Add clearing condition",
"condition-duration": "Condition duration", "condition-duration": "Condition duration",
@ -1498,6 +1506,7 @@
"condition-duration-value-required": "Duration value is required.", "condition-duration-value-required": "Duration value is required.",
"condition-duration-time-unit-required": "Time unit is required.", "condition-duration-time-unit-required": "Time unit is required.",
"condition-repeating-value": "Count of events", "condition-repeating-value": "Count of events",
"condition-repeating-value-hint": "Update of any alarm rule argument will be counted as event",
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.", "condition-repeating-value-required": "Count of events is required.",
@ -1513,12 +1522,22 @@
"alarm-rule-filter-title": "Filter", "alarm-rule-filter-title": "Filter",
"debugging": "Alarm rule debugging", "debugging": "Alarm rule debugging",
"any-type": "Any type", "any-type": "Any type",
"enter-alarm-rule-type": "Enter alarm rule type", "enter-alarm-rule-type": "Enter alarm type",
"no-alarm-rule-types-matching": "No alarm rule types matching '{{entitySubtype}}' were found.", "no-alarm-rule-types-matching": "No alarm types matching '{{entitySubtype}}' were found.",
"alarm-rule-type-list-empty": "No alarm rule types selected.", "alarm-rule-type-list-empty": "No alarm types selected.",
"alarm-rule-type-list": "Alarm rule type list", "alarm-rule-type-list": "Alarm type list",
"alarm-rule-entity-list": "Entity list", "alarm-rule-entity-list": "Entity list",
"import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule type." "missing-for": "missing for",
"time-unit": "Unit",
"value-required": "Value is required.",
"min-value": "Value must be 0 or greater.",
"argument-in-use": "Argument is used as general argument.",
"import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule structure.",
"no-filter-preview": "No filter specified",
"filter-operation": {
"and": "And",
"or": "Or"
}
}, },
"ai-models": { "ai-models": {
"ai-models": "AI models", "ai-models": "AI models",
@ -7176,7 +7195,8 @@
"export-relations": "Export relations", "export-relations": "Export relations",
"export-attributes": "Export attributes", "export-attributes": "Export attributes",
"export-credentials": "Export credentials", "export-credentials": "Export credentials",
"export-calculated-fields": "Export calculated fields", "export-calculated-fields": "Export calculated fields \nand alarm rules",
"export-alarm-rules": "Export alarm rules",
"entity-versions": "Entity versions", "entity-versions": "Entity versions",
"versions": "Versions", "versions": "Versions",
"created-time": "Created time", "created-time": "Created time",

Loading…
Cancel
Save