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
private long cfCheckInterval;
@Value("${actors.alarms.reevaluation_interval:120}")
@Value("${actors.alarms.reevaluation_interval:60}")
@Getter
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);
}
} 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(() -> {
try {
calculatedFields.values().forEach(cf -> {
if (cf.isRequiresScheduledReevaluation()) {
if (cf.requiresScheduledReevaluation()) {
applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> {
log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId());
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.DeviceId;
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.TenantProfileId;
import org.thingsboard.server.common.data.page.PageDataIterable;
@ -243,11 +244,12 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
@Override
public EntityId getProfileId(TenantId tenantId, EntityId entityId) {
return switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId();
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId();
HasId<? extends EntityId> profile = switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId);
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId);
default -> null;
};
return profile != null ? profile.getId() : null;
}
@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;
}
public boolean isRequiresScheduledReevaluation() {
public boolean requiresScheduledReevaluation() {
long now = System.currentTimeMillis();
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) {
Watermark watermark = entityAggregationConfig.getWatermark();
@ -232,8 +232,8 @@ public class CalculatedFieldCtx implements Closeable {
}
boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation();
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval());
if (requiresScheduledReevaluation) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval());
if (now - lastReevaluationTs >= reevaluationIntervalMillis) {
lastReevaluationTs = now;
return true;
@ -642,6 +642,9 @@ public class CalculatedFieldCtx implements Closeable {
// if the rules have any changes not tracked by hasStateChanges
return true;
}
if (!thisConfig.propagationSettingsEqual(otherConfig)) {
return true;
}
}
if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) {
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.ctx.CalculatedFieldEntityCtxId;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
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();
}
@Override
public void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions) {
stateService.update(queueKey, partitions, null);
}
@Override
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());

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.ComplexFilterPredicate;
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.StringFilterPredicate;
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.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.NOT_YET_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);
}
}
return evalResult;
return evalResult.withCause(NEW_EVENT);
} else {
return state.reeval(System.currentTimeMillis(), ctx);
return state.reeval(System.currentTimeMillis(), ctx).withCause(SCHEDULED_REEVALUATION);
}
}, ctx);
return Futures.immediateFuture(AlarmCalculatedFieldResult.builder()
@ -247,12 +250,12 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
private TbAlarmResult createOrClearAlarms(Function<AlarmRuleState, AlarmEvalResult> evalFunction,
CalculatedFieldCtx ctx) {
TbAlarmResult result = null;
AlarmEvalResult evalResult = null;
AlarmRuleState resultState = null;
AlarmRuleState.StateInfo resultStateInfo = null;
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);
if (evalResult.getStatus() == TRUE) {
resultState = state;
@ -262,13 +265,14 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
}
}
TbAlarmResult result = null;
if (resultState != null) {
result = calculateAlarmResult(resultState, ctx);
result = calculateAlarmResult(resultState, evalResult, ctx);
resultStateInfo = resultState.getStateInfo();
log.debug("Alarm result for state {}: {}", resultState, result);
clearState(clearRuleState);
} 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);
if (evalResult.getStatus() == TRUE) {
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();
if (currentAlarm != null) {
currentAlarm.setEndTs(System.currentTimeMillis());
AlarmSeverity oldSeverity = currentAlarm.getSeverity();
// Skip update if severity is decreased.
if (severity.ordinal() <= oldSeverity.ordinal()) {
currentAlarm.setDetails(createDetails(ruleState));
currentAlarm.setSeverity(severity);
AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
} else {
if (severity.ordinal() > oldSeverity.ordinal()) {
log.trace("Skipping alarm update for result state {} for eval result {} because severity is decreased", ruleState, evalResult);
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 {
var newAlarm = new Alarm();
newAlarm.setType(alarmType);
@ -338,7 +346,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
newAlarm.setSeverity(severity);
long startTs = latestTimestamp;
long currentTime = System.currentTimeMillis();
if (startTs == 0L || startTs > currentTime) {
if (startTs <= 0L || startTs > currentTime) {
startTs = currentTime;
}
newAlarm.setStartTs(startTs);
@ -428,6 +436,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate);
case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate);
case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate);
case NO_DATA -> evalNoDataPredicate(argument, (NoDataFilterPredicate) 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) {
T value = conditionValue.getStaticValue();
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 long leftDuration;
private final long leftEvents;
private Cause cause;
public AlarmEvalResult(Status status) {
this(status, 0, 0);
@ -39,8 +40,17 @@ public class AlarmEvalResult {
return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents);
}
public AlarmEvalResult withCause(Cause cause) {
this.cause = cause;
return this;
}
public enum Status {
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.concurrent.ScheduledFuture;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE;
@Data
@Slf4j
public class AlarmRuleState {
@ -81,16 +83,20 @@ public class AlarmRuleState {
boolean active = isActive(ts);
switch (condition.getType()) {
case SIMPLE, REPEATING -> {
if (this.active == null || active != this.active) {
this.active = active;
if (active) {
return doEval(false, ctx);
}
boolean activeChanged = this.active == null || active != this.active;
this.active = active;
if (!active) {
return AlarmEvalResult.EMPTY;
}
if (active) {
return AlarmEvalResult.NOT_YET_TRUE;
} else {
return AlarmEvalResult.FALSE;
if ((condition.hasSchedule() && activeChanged) ||
condition.getExpression().requiresScheduledReevaluation()) {
AlarmEvalResult result = doEval(false, ctx);
if (result.getStatus() == TRUE) {
return result;
} else {
return AlarmEvalResult.EMPTY;
}
}
}
case DURATION -> {
@ -116,7 +122,7 @@ public class AlarmRuleState {
}
}
}
return AlarmEvalResult.FALSE;
return AlarmEvalResult.EMPTY;
}
public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) {
@ -338,6 +344,7 @@ public class AlarmRuleState {
}
public record StateInfo(Long eventCount, Long duration) {
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;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
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.HasVersion;
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.geofencing.GeofencingCalculatedFieldConfiguration;
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;
}

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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
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.audit.ActionType;
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.geofencing.GeofencingCalculatedFieldConfiguration;
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();
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}"
alarms:
# 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:
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.SimpleAlarmConditionExpression;
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.NumericOperation;
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
public void testChangeAlarmType() throws Exception {
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.DeviceProfileType;
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.SaveDeviceWithCredentialsRequest;
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.asset.AssetProfile;
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.device.data.DefaultDeviceConfiguration;
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");
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
< System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() <
System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, 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 {
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();
}
@ -1413,12 +1415,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected void postTelemetry(EntityId entityId, String payload) throws Exception {
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 {
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) {
@ -1427,7 +1429,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception {
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 {

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

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -56,9 +55,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -295,23 +292,6 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
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
public void testDeleteCalculatedField() throws Exception {
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.jdbc.core.JdbcTemplate;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
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.StringFilterPredicate;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import java.util.ArrayList;
import java.util.Collections;
@ -62,8 +64,11 @@ public class EdqsControllerTest extends AbstractControllerTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@Before
public void beforeEdqsControllerTest() throws Exception {
public void before() throws Exception {
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.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.edqs.EdqsState;
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.msg.edqs.EdqsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import java.util.concurrent.TimeUnit;
@ -59,9 +57,6 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
@Autowired
private DiscoveryService discoveryService;
@MockBean // so that we don't do backup for tests
private EdqsRocksDb edqsRocksDb;
@Before
public void before() {
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.ResultActions;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
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.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import java.util.ArrayList;
import java.util.Arrays;
@ -102,6 +104,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
@Autowired
private QueueStatsService queueStatsService;
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -436,7 +441,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
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);
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");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields);
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields);
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
@ -509,16 +514,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
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();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList());
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList());
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
@ -574,7 +579,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
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);
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);
}
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()
.alias("data by query")
.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.Test;
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.bean.override.mockito.MockitoBean;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.CustomerId;
@ -61,7 +61,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest {
@Autowired
private EdqsService edqsService;
@MockBean
@MockitoBean
private EdqsRocksDb edqsRocksDb;
@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.TextNode;
import com.google.common.collect.Streams;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Before;
import org.junit.Test;
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.Tenant;
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.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
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.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
@ -306,6 +312,54 @@ public class VersionControlTest extends AbstractControllerTest {
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
public void testDashboardVc_betweenTenants() throws Exception {
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 unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa";
String entityAliases = "{\n" +
"\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" +
"\"filter\": {\n" +
" \"entityList\": [\n" +
" \"" + asset1.getId() + "\",\n" +
" \"" + asset2.getId() + "\",\n" +
" \"" + tenantId1.getId() + "\",\n" +
" \"" + existingDeviceProfile.getId() + "\",\n" +
" \"" + unknownUuid + "\"\n" +
" ],\n" +
" \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" +
"},\n" +
"\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}";
"\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" +
"\"filter\": {\n" +
" \"entityList\": [\n" +
" \"" + asset1.getId() + "\",\n" +
" \"" + asset2.getId() + "\",\n" +
" \"" + tenantId1.getId() + "\",\n" +
" \"" + existingDeviceProfile.getId() + "\",\n" +
" \"" + unknownUuid + "\"\n" +
" ],\n" +
" \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" +
"},\n" +
"\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}";
String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b";
String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae";
String widgets = "{\n" +
" \"" + widgetId + "\": {\n" +
" \"config\": {\n" +
" \"actions\": {\n" +
" \"rowClick\": [\n" +
" {\n" +
" \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"row\": 0,\n" +
" \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" +
" }\n" +
"}";
" \"" + widgetId + "\": {\n" +
" \"config\": {\n" +
" \"actions\": {\n" +
" \"rowClick\": [\n" +
" {\n" +
" \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"row\": 0,\n" +
" \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" +
" }\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
@ -499,11 +553,12 @@ public class VersionControlTest extends AbstractControllerTest {
generatorNodeConfig.setMsgCount(1);
generatorNodeConfig.setScriptLang(ScriptLanguage.JS);
UUID someUuid = UUID.randomUUID();
generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
"var metadata = { data: 40 };\n" +
"var msgType = \"POST_TELEMETRY_REQUEST\";\n" +
"var someUuid = \"" + someUuid + "\";\n" +
"return { msg: msg, metadata: metadata, msgType: msgType };");
generatorNodeConfig.setJsScript("""
var msg = { temp: 42, humidity: 77 };
var metadata = { data: 40 };
var msgType = "POST_TELEMETRY_REQUEST";
var someUuid = "%s";
return { msg: msg, metadata: metadata, msgType: msgType };""".formatted(someUuid));
generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig));
nodes.add(generatorNode);
metaData.setNodes(nodes);
@ -1018,20 +1073,21 @@ public class VersionControlTest extends AbstractControllerTest {
protected Dashboard createDashboard(CustomerId customerId, String name, AssetId assetForEntityAlias) {
Dashboard dashboard = createDashboard(customerId, name);
String entityAliases = "{\n" +
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" +
"\t\t\"alias\": \"assets\",\n" +
"\t\t\"filter\": {\n" +
"\t\t\t\"entityList\": [\n" +
"\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" +
"\t\t\t],\n" +
"\t\t\t\"entityType\": \"ASSET\",\n" +
"\t\t\t\"resolveMultiple\": true,\n" +
"\t\t\t\"type\": \"entityList\"\n" +
"\t\t},\n" +
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" +
"\t}\n" +
"}";
String entityAliases = """
{
"23c4185d-1497-9457-30b2-6d91e69a5b2c": {
"alias": "assets",
"filter": {
"entityList": [
"%s"
],
"entityType": "ASSET",
"resolveMultiple": true,
"type": "entityList"
},
"id": "23c4185d-1497-9457-30b2-6d91e69a5b2c"
}
}""".formatted(assetForEntityAlias.getId().toString());
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("description", new TextNode("hallo"));
@ -1134,6 +1190,33 @@ public class VersionControlTest extends AbstractControllerTest {
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) {
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);
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.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)));
// 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()))));
// mock TPI for entity view
given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi);
var request = TimeseriesSaveRequest.builder()
.tenantId(tenantId)
@ -373,8 +371,6 @@ class DefaultTelemetrySubscriptionServiceTest {
lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView)));
// 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()))));
// mock TPI for entity view
lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).thenReturn(tpi);
var request = TimeseriesSaveRequest.builder()
.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) {
final Caffeine<Object, Object> caffeineBuilder
= Caffeine.newBuilder()
Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder()
.weigher(collectionSafeWeigher())
.maximumWeight(cacheSpec.getMaxSize())
.recordStats()
.ticker(ticker());
if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) {
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 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.AlarmCondition;
import org.thingsboard.server.common.data.id.DashboardId;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AlarmRule {
@Valid
@ -33,7 +37,7 @@ public class AlarmRule {
@JsonIgnore
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.JsonTypeInfo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
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.schedule.AlarmSchedule;
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);
}
@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
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
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.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
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.query.EntityKeyValueType;
@ -34,7 +37,21 @@ public class AlarmConditionFilter implements Serializable {
private EntityKeyValueType valueType;
private ComplexOperation operation;
@Valid
@NotNull
@NotEmpty
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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType;
import java.util.List;
@ -38,4 +39,9 @@ public class SimpleAlarmConditionExpression implements AlarmConditionExpression
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;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TbelAlarmConditionExpression implements AlarmConditionExpression {
@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,
NUMERIC,
BOOLEAN,
NO_DATA,
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 = NumericFilterPredicate.class, name = "NUMERIC"),
@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 {
@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.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> {
@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,
{
id: MenuId.alarms,
name: 'alarm.alarms',
name: 'alarm.alarm-list',
type: 'link',
path: '/alarms/alarms',
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">
<textarea matInput [formControl]="alarmDetailsControl" rows="5"
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>
<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 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 }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
<div class="tb-form-panel stroked no-padding no-gap">
<div class="tb-form-row no-border">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ '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>
@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">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner">
{{ '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>
</div>
<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-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
@ -78,7 +78,7 @@
</div>
@if (alarmRuleFilterConfigForm.get('entityType').value) {
<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"
subscriptSizing="dynamic"
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 { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.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({
declarations: [
@ -69,7 +72,8 @@ import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alar
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
AlarmRuleFilterConfigComponent,
AlarmRuleTableHeaderComponent
AlarmRuleTableHeaderComponent,
AlarmRuleFilterPredicateNoDataValueComponent
],
imports: [
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%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
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)));
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 => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false));
}
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(', '),
() => ({}), 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));
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>
@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">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.condition-type</mat-label>
<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 }}
</mat-option>
</mat-select>
@if (isNoData) {
<mat-hint translate>alarm-rule.condition-type-hint</mat-hint>
}
</mat-form-field>
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
<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>
</div>
<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>
@ -162,9 +194,8 @@
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty">
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty || !filtersValid">
{{ 'action.save' | translate }}
</button>
}
@ -186,6 +217,9 @@
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) {
<mat-error>{{ defaultValuePatternError | translate }}</mat-error>
}
@if (type === AlarmConditionType.REPEATING) {
<mat-hint translate>alarm-rule.condition-repeating-value-hint</mat-hint>
}
</mat-form-field>
</div>
</ng-template>
@ -198,9 +232,14 @@
<mat-option [value]="argument">{{ argument }}</mat-option>
}
</mat-select>
<mat-error *ngIf="conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')">
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
@if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) {
<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>
</ng-container>
</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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from "@shared/models/rule-node.models";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType
} from "@shared/models/alarm-rule.models";
import {
CalculatedFieldArgument,
getCalculatedFieldArgumentsEditorCompleter,
@ -38,8 +31,18 @@ import {
} from "@shared/models/calculated-field.models";
import { TbEditorCompleter } from "@shared/models/ace/completion.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 { TranslateService } from "@ngx-translate/core";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType,
AlarmRuleFilter,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
@ -96,7 +99,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
repeatingDynamicModeControl = this.fb.control<boolean>(false);
ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap;
complexOperationTranslationMap = filterOperationTranslationMap;
specText = '';
filtersValid: boolean = false;
functionArgs: Array<string>;
argumentsEditorCompleter: TbEditorCompleter;
@ -105,11 +112,14 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
arguments = this.data.arguments;
argumentsList: Array<string>;
isNoData: boolean = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>,
private fb: FormBuilder) {
private fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
@ -128,21 +138,38 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: {
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: {
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});
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.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(
takeUntilDestroyed()
).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(
@ -164,6 +191,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
});
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');
}
@ -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') {
if (type === 'SIMPLE') {
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) {
case AlarmRuleConditionType.DURATION:
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 {
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"
type="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)">
<div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
@ -32,20 +32,23 @@
required
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</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>
</button>
</div>
<div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div>
<button [disabled]="disabled"
[color]="schedulerArgumentsValid ? 'primary' : 'warn'"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button color="primary"
mat-stroked-button
(click)="openScheduleDialog($event)">
<div class="flex items-center gap-2">
<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>
</button>
</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.
///
import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
@ -68,7 +69,7 @@ import { Observable } from "rxjs";
}
]
})
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator {
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges {
@Input()
@coerceBoolean()
@ -96,11 +97,15 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
specText = '';
filtersArgumentsValid: boolean = true;
schedulerArgumentsValid: boolean = true;
scheduleText = '';
private modelValue: AlarmRuleCondition;
private propagateChange = (v: any) => { };
private onValidatorChange = () => { };
constructor(private dialog: MatDialog,
private fb: FormBuilder,
@ -115,6 +120,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
registerOnTouched(fn: any): void {
}
registerOnValidatorChange(fn: () => void): void {
this.onValidatorChange = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -127,14 +136,68 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
writeValue(value: AlarmRuleCondition): void {
this.modelValue = value;
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() {
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
}
public validate(): ValidationErrors | null {
return this.conditionSet() ? null : {
public validate(control: AbstractControl): ValidationErrors | 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: {
valid: false,
}
@ -226,6 +289,9 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
private updateModel() {
this.updateConditionInfo();
this.propagateChange(this.modelValue);
if (this.modelValue) {
this.onValidatorChange();
}
}
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>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<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
</div>
<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"
matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)">
@ -42,7 +42,7 @@
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'alarm-rule.alarm-rule-mobile-dashboard' | translate }}"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
</div>

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

@ -20,6 +20,11 @@
display: grid;
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 {
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) {
this.modelValue = value;
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 {
switch (this.modelValue.staticValue.type) {
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>
</mat-toolbar>
<div mat-dialog-content>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
<section class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ data.valueType === EntityKeyValueType.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filters' | translate }}
</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="complexOperationEnum.AND">{{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="complexOperationEnum.OR">{{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</section>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<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 { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { ComplexOperation, EntityKeyValueType } from '@shared/models/query/query.models';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType
} from '@shared/models/query/query.models';
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
ComplexAlarmRuleFilterPredicate,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export interface AlarmRuleComplexFilterPredicateDialogData {
@ -55,9 +55,9 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
}
);
complexOperations = Object.keys(ComplexOperation);
EntityKeyValueType = EntityKeyValueType;
complexOperationEnum = ComplexOperation;
complexOperationTranslations = complexOperationTranslationMap;
complexOperationTranslations = filterOperationTranslationMap;
isAdd: boolean;
@ -81,7 +81,7 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
save(): void {
const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate;
predicate.type = FilterPredicateType.COMPLEX;
predicate.type = AlarmRuleFilterPredicateType.COMPLEX;
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="tb-form-panel-title"
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>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
@ -95,7 +95,7 @@
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty">
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty || !predicatesValid">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</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 { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypesMap
} from '@shared/models/query/query.models';
import { ComplexOperation, EntityKeyValueType, entityKeyValueTypesMap } from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service';
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 { FormControlsFrom } from "@shared/models/tenant.model";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { isDefinedAndNotNull } from "@core/utils";
export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter;
@ -57,7 +57,9 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
entityKeyValueTypes = entityKeyValueTypesMap;
complexOperationTranslationMap = complexOperationTranslationMap;
complexOperationTranslationMap = filterOperationTranslationMap;
predicatesValid: boolean = false;
ComplexOperation = ComplexOperation;
@ -78,12 +80,20 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
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]],
predicates: [this.data.filter.predicates, [Validators.required]],
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(
takeUntilDestroyed(this.destroyRef)
).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 {
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 class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1">{{ filterControl.value?.argument }}</div>
<div class="flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}</div>
<button mat-icon-button color="primary"
<div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div>
<div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>{{'edit'}}</mat-icon>
<mat-icon
[matBadgeHidden]="areFilterAndPredicateArgumentsValid(filterControl.value)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
@ -62,14 +69,15 @@
}
</div>
}
<span [class.!hidden]="!!filtersFormArray.length"
class="no-data-found flex items-center justify-center"
translate>
filter.no-key-filters
</span>
@if (!filtersFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div>
</section>
<button mat-button mat-raised-button color="primary"
<button mat-button mat-stroked-button color="primary"
(click)="addFilter()"
type="button">
{{ '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 {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.filters-text {
font-size: 14px;
}
}
.filters-operation {
display: flex;
@ -39,7 +43,8 @@
background-color: white;
}
&-label {
font-weight: 500;
font-size: 15px;
font-weight: 400;
color: #00695C;
padding: 0 8px;
border-radius: 4px;
@ -47,4 +52,10 @@
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 {
if (isDisabled) {
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>
}
<span [class.!hidden]="!!predicatesFormArray.length"
[class.disabled]="disabled"
class="no-data-found flex items-center justify-center" translate>
filter.no-filters
</span>
@if (!predicatesFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div>
</div>
<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"
(click)="addPredicate(false)"
type="button">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
<button mat-button
color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
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;
}
&-label {
font-weight: 500;
font-size: 15px;
font-weight: 400;
color: #00695C;
padding: 0 8px;
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';
import { Observable, of } from 'rxjs';
import {
BooleanOperation,
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypeToFilterPredicateType,
FilterPredicateType,
NumericOperation,
StringOperation
entityKeyValueTypeToFilterPredicateType
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { map } from 'rxjs/operators';
@ -44,8 +40,12 @@ import {
AlarmRuleComplexFilterPredicateDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
import {
AlarmRuleBooleanOperation,
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
AlarmRuleNumericOperation,
AlarmRulePredicateInfo,
AlarmRuleStringOperation,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.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 {
const predicate = {
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
type: complex ? AlarmRuleFilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
} as AlarmRuleFilterPredicate;
switch (predicate.type) {
case FilterPredicateType.STRING:
predicate.operation = StringOperation.STARTS_WITH;
case AlarmRuleFilterPredicateType.STRING:
predicate.operation = AlarmRuleStringOperation.STARTS_WITH;
predicate.value = {
staticValue: ''
};
predicate.ignoreCase = false;
break;
case FilterPredicateType.NUMERIC:
predicate.operation = NumericOperation.EQUAL;
case AlarmRuleFilterPredicateType.NUMERIC:
predicate.operation = AlarmRuleNumericOperation.EQUAL;
predicate.value = {
staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0
};
break;
case FilterPredicateType.BOOLEAN:
predicate.operation = BooleanOperation.EQUAL;
case AlarmRuleFilterPredicateType.BOOLEAN:
predicate.operation = AlarmRuleBooleanOperation.EQUAL;
predicate.value = {
staticValue: false
};
break;
case FilterPredicateType.COMPLEX:
case AlarmRuleFilterPredicateType.COMPLEX:
predicate.operation = ComplexOperation.AND;
predicate.predicates = [];
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
</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>
}
</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.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -31,6 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmRuleValue } from "@shared/models/alarm-rule.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { isDefinedAndNotNull } from "@core/utils";
@Component({
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()
arguments: Record<string, CalculatedFieldArgument>;
@ -110,6 +111,20 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
).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 {
if (isDisabled) {
this.filterPredicateValueFormGroup.disable({emitEvent: false});
@ -125,6 +140,12 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
if (isDynamicMode) {
this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false});
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 {
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('staticValue').enable();
@ -145,8 +166,14 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
}
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.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false});
}
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-select>
</mat-form-field>
<mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true">
<mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option>
</mat-chip-listbox>
@if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) {
<mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true">
<mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option>
</mat-chip-listbox>
}
</div>
}
@case (filterPredicateType.NUMERIC) {
@ -58,7 +60,7 @@
<div class="tb-form-row no-border no-padding flex-1">
<button type="button" style="--mat-outlined-button-horizontal-padding: 3px 0px 12px;"
class="block w-full"
mat-stroked-button color="primary"
mat-stroked-button [color]="predicateValid ? 'primary' : 'warn'"
(click)="openComplexFilterDialog()">
<div class="flex items-center gap-2">
<span class="w-full text-start" translate>filter.complex-filter</span>
@ -68,7 +70,14 @@
</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"
[arguments]="arguments"
[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,
Validator
} from '@angular/forms';
import {
BooleanOperation,
booleanOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
numericOperationTranslationMap,
StringOperation,
stringOperationTranslationMap
} from '@shared/models/query/query.models';
import { EntityKeyValueType } from '@shared/models/query/query.models';
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 {
AlarmRuleComplexFilterPredicateDialogComponent,
@ -77,24 +78,27 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
operation: [],
ignoreCase: false,
predicates: [],
value: []
value: [],
duration: []
});
type: FilterPredicateType;
type: AlarmRuleFilterPredicateType;
filterPredicateType = AlarmRuleFilterPredicateType;
filterPredicateType = FilterPredicateType;
stringOperations = Object.keys(AlarmRuleStringOperation);
stringOperation = AlarmRuleStringOperation;
stringOperationTranslationMap = alarmRuleStringOperationTranslationMap;
stringOperations = Object.keys(StringOperation);
stringOperation = StringOperation;
stringOperationTranslationMap = stringOperationTranslationMap;
numericOperations = Object.keys(AlarmRuleNumericOperation);
numericOperationEnum = AlarmRuleNumericOperation;
numericOperationTranslations = alarmRuleNumericOperationTranslationMap;
numericOperations = Object.keys(NumericOperation);
numericOperationEnum = NumericOperation;
numericOperationTranslations = numericOperationTranslationMap;
booleanOperations = Object.keys(AlarmRuleBooleanOperation);
booleanOperationEnum = AlarmRuleBooleanOperation;
booleanOperationTranslations = alarmRuleBooleanOperationTranslationMap;
booleanOperations = Object.keys(BooleanOperation);
booleanOperationEnum = BooleanOperation;
booleanOperationTranslations = booleanOperationTranslationMap;
predicateValid: boolean = false;
private propagateChange= (v: any) => { };
@ -106,6 +110,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
).subscribe(() => {
this.updateModel();
});
this.filterPredicateFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicateValid = this.isPredicateArgumentsValid(predicates);
})
}
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 {
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() {
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() {

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

@ -19,5 +19,6 @@
tbTruncateWithTooltip
[class.required]="isRequired"
[class.nowrap]="nowrap"
[class.wrap]="!nowrap"
[innerHTML]="filterText">
</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;
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 {
booleanOperationTranslationMap,
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType,
numericOperationTranslationMap,
stringOperationTranslationMap
EntityKeyValueType
} from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import {
alarmRuleBooleanOperationTranslationMap,
AlarmRuleExpression,
AlarmRuleExpressionType,
AlarmRuleFilter,
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
alarmRuleNumericOperationTranslationMap,
AlarmRuleStringOperation,
alarmRuleStringOperationTranslationMap,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { coerceBoolean } from "@shared/decorators/coercion";
import { timeUnitTranslationMap } from "@shared/models/time/time.models";
@Component({
selector: 'tb-alarm-rule-filter-text',
@ -96,7 +98,7 @@ export class AlarmRuleFilterTextComponent {
private updateFilterText(value: AlarmRuleExpression) {
this.isRequired = false;
if (value && (value.expression || value.filters)) {
if (value && (value.expression || value.filters?.length)) {
if (value.type === AlarmRuleExpressionType.SIMPLE) {
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
} else {
@ -137,21 +139,21 @@ export class AlarmRuleFilterTextComponent {
const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND);
const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => {
if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
if (keyFilterPredicate.type === AlarmRuleFilterPredicateType.COMPLEX) {
const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate;
const complexOperation = complexPredicate.operation ?? ComplexOperation.AND;
return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation);
} else {
let operation: string;
let value: string;
const val = keyFilterPredicate.value;
const val = keyFilterPredicate.type === AlarmRuleFilterPredicateType.NO_DATA ? keyFilterPredicate.duration : keyFilterPredicate.value;
const dynamicValue = val?.dynamicValueArgument?.length;
if (dynamicValue) {
value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-value">' + val?.dynamicValueArgument + '</span></span>';
}
switch (keyFilterPredicate.type) {
case FilterPredicateType.STRING:
operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation));
case AlarmRuleFilterPredicateType.STRING:
operation = translate.instant(alarmRuleStringOperationTranslationMap.get(keyFilterPredicate.operation));
if (keyFilterPredicate.ignoreCase) {
operation += ' ' + translate.instant('filter.ignore-case');
}
@ -159,8 +161,8 @@ export class AlarmRuleFilterTextComponent {
value = `'${keyFilterPredicate.value.staticValue}'`;
}
break;
case FilterPredicateType.NUMERIC:
operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation));
case AlarmRuleFilterPredicateType.NUMERIC:
operation = translate.instant(alarmRuleNumericOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) {
if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) {
value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm');
@ -169,12 +171,18 @@ export class AlarmRuleFilterTextComponent {
}
}
break;
case FilterPredicateType.BOOLEAN:
operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation));
case AlarmRuleFilterPredicateType.BOOLEAN:
operation = translate.instant(alarmRuleBooleanOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) {
value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false');
}
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) {
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',
hiddenDefaultValue: true,
hiddenEntityKeyTypes: true,
watchKeyChange: true,
};
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';
} else if (!this.argumentsFormArray.controls.length) {
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';
} else {
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,
defaultValueRequired: true,
argumentEntityTypes: [ArgumentEntityType.Current],
hint: 'calculated-fields.hint.setting-arguments-aggregation'
hint: 'calculated-fields.hint.setting-arguments-aggregation',
watchKeyChange: true,
};
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 class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value">
<div class="tb-form-panel-title tb-required">
{{ 'calculated-fields.expression' | translate }}
{{ 'calculated-fields.script' | translate }}
</div>
<div>
<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);
if (this.isScript) {
formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript;
} else {
} else if (value.type === CalculatedFieldType.SIMPLE) {
formValue.expressionSIMPLE = formValue.expression;
}
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">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields" class="checkbox-pre-line">
{{ (entityTypeFormGroup.get('entityType').value === entityTypes.CUSTOMER ? 'version-control.export-alarm-rules' : 'version-control.export-calculated-fields') | translate }}
</mat-checkbox>
</div>
</div>

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

@ -56,6 +56,17 @@
}
: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-body {
padding: 0;

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

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

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

@ -120,7 +120,8 @@ export interface AlarmRulePredicateInfo {
export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate |
NumericAlarmRuleFilterPredicate |
BooleanAlarmRuleFilterPredicate |
ComplexAlarmRuleFilterPredicate;
ComplexAlarmRuleFilterPredicate |
NoDataAlarmRuleFilterPredicate;
export interface AlarmRuleValue<T> {
dynamicValueArgument?: string;
@ -128,26 +129,33 @@ export interface AlarmRuleValue<T> {
}
export interface StringAlarmRuleFilterPredicate {
type: FilterPredicateType.STRING;
operation: StringOperation;
type: AlarmRuleFilterPredicateType.STRING;
operation: AlarmRuleStringOperation;
value: AlarmRuleValue<string>;
ignoreCase: boolean;
}
export interface NumericAlarmRuleFilterPredicate {
type: FilterPredicateType.NUMERIC;
operation: NumericOperation;
type: AlarmRuleFilterPredicateType.NUMERIC;
operation: AlarmRuleNumericOperation;
value: AlarmRuleValue<number>;
}
export interface BooleanAlarmRuleFilterPredicate {
type: FilterPredicateType.BOOLEAN;
operation: BooleanOperation;
type: AlarmRuleFilterPredicateType.BOOLEAN;
operation: AlarmRuleBooleanOperation;
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> {
type: FilterPredicateType.COMPLEX;
type: AlarmRuleFilterPredicateType.COMPLEX;
operation: ComplexOperation;
predicates: Array<T>;
}
@ -160,6 +168,83 @@ export interface AlarmRuleFilterConfig {
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 =
'// Sample expression for an alarm rule: triggers when temperature is above 20 degree\n' +
'return temperature > 20;'

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

@ -265,4 +265,4 @@ export interface EntityDataInfo {
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-list": "Alarm list",
"alarms": "Alarms",
"all-alarms": "All alarms",
"select-alarm": "Select alarm",
@ -1209,8 +1210,9 @@
"propagation-path-related-entities": "Propagation path to related entities",
"propagate-type": {
"arguments-only": "Arguments only",
"expression-result": "Expression result"
"expression-result": "Calculation result"
},
"script": "Script",
"data-propagate": "Data to propagate",
"output-key": "Output key",
"copy-output-key": "Copy output key",
@ -1368,7 +1370,7 @@
"arguments-aggregation": "Defines input parameters used for filtering and aggregation.",
"setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.",
"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": {
@ -1378,7 +1380,7 @@
"alarm-rules-old": "Old",
"alarm-rules-actual": "Actual",
"severities": "Severities",
"cleared": "Clear condition",
"cleared": "Clearing condition",
"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-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",
"target-entity-type": "Target entity type",
"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-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.",
@ -1412,7 +1415,7 @@
"condition-during": "During {{during}}",
"condition-during-dynamic": "During \"{{ attribute }}\"",
"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",
"condition-settings": "Condition settings",
"static": "Static",
@ -1421,7 +1424,7 @@
"argument-name": "Argument name",
"value-type": "Value type",
"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.",
"operation": "Operation",
"value-source": "Value source",
@ -1429,8 +1432,11 @@
"ignore-case": "Ignore case",
"condition": "Condition",
"script": "Script",
"add-filter": "Add filter",
"edit-filter": "Edit filter",
"add-filter": "Add argument 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": {
"simple": "Simple",
"duration": "Duration",
@ -1478,16 +1484,18 @@
"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-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-hint": "Used by mobile application as an alarm details dashboard",
"alarm-rule-no-mobile-dashboard": "No dashboard selected",
"alarm-rule-condition": "Alarm rule condition",
"enter-alarm-rule-condition-prompt": "Add alarm rule creating condition",
"enter-alarm-rule-clear-condition-prompt": "Add alarm rule clearing condition",
"edit-alarm-rule-condition": "Edit alarm rule condition",
"enter-alarm-rule-condition-prompt": "Add condition",
"enter-alarm-rule-clear-condition-prompt": "Add clearing condition",
"edit-alarm-rule-condition": "Alarm condition",
"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",
"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-clear-alarm-rule": "Add clearing condition",
"condition-duration": "Condition duration",
@ -1498,6 +1506,7 @@
"condition-duration-value-required": "Duration value is required.",
"condition-duration-time-unit-required": "Time unit is required.",
"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-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
@ -1513,12 +1522,22 @@
"alarm-rule-filter-title": "Filter",
"debugging": "Alarm rule debugging",
"any-type": "Any type",
"enter-alarm-rule-type": "Enter alarm rule type",
"no-alarm-rule-types-matching": "No alarm rule types matching '{{entitySubtype}}' were found.",
"alarm-rule-type-list-empty": "No alarm rule types selected.",
"alarm-rule-type-list": "Alarm rule type list",
"enter-alarm-rule-type": "Enter alarm type",
"no-alarm-rule-types-matching": "No alarm types matching '{{entitySubtype}}' were found.",
"alarm-rule-type-list-empty": "No alarm types selected.",
"alarm-rule-type-list": "Alarm type 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",
@ -7176,7 +7195,8 @@
"export-relations": "Export relations",
"export-attributes": "Export attributes",
"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",
"versions": "Versions",
"created-time": "Created time",

Loading…
Cancel
Save