diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index bb8c3fca44..654ae8bf29 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/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; diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index da16e55db8..cfac432017 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/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 { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 4022526aab..b19ad1a8b4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/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)); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index f2b8d4d9db..e7c4801c12 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/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 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 diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index a6234ad3cc..f04f3b109a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/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; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index e8174bfd57..ffc3c1584b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/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>) queueFactory.createCalculatedFieldStateProducer(); } - @Override - public void restore(QueueKey queueKey, Set 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()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java index 342f7534c2..bd2c272bca 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java +++ b/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 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 resolveValue(AlarmConditionValue conditionValue, Function mapper) { T value = conditionValue.getStaticValue(); if (value == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java index 4f1a8638ee..2569f837fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java +++ b/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; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java index 4c189887b4..c6a5cbf418 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java +++ b/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); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index 5d634c4178..70e90b2636 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/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 { + if (rule.getDashboardId() != null) { + rule.setDashboardId(getExternalIdOrElseInternal(ctx, rule.getDashboardId())); + } + }); + } }); return calculatedFields; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index c95fffc205..61aca6eb8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/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 { + if (rule.getDashboardId() != null) { + rule.setDashboardId(idProvider.getInternalId(rule.getDashboardId(), ctx.isFinalImportAttempt())); + } + }); + } }).toList(); for (CalculatedField existingField : existing) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4ffa7f31d2..d3d04bcea0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/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: diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index 2ac5d59b3a..5f91dab190 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/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 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(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(criticalNoDataDuration, null)) + )); + criticalExpression.setFilters(List.of(criticalFilter)); + + Map 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(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 51d77bd134..80e79280df 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/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 statesMap = (Map) 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 findJobs(List types, List 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>() {}, 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 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 getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", + new TypeReference>() {}, pageLink); + } + + protected List getCalculatedFields(CalculatedFieldType type, + EntityType entityType, + List entities, + List 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>() {}, new PageLink(10)).getData(); } protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 635fc747e5..33f6bfa250 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/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 getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { - return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", - new TypeReference>() {}, pageLink); - } - - private List getCalculatedFields(CalculatedFieldType type, - EntityType entityType, - List entities, - List 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>() {}, new PageLink(10)).getData(); - } - @Test public void testDeleteCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java index 91be3f4744..c3f11ce8d8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java +++ b/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(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 655ab417e6..dca25a83a1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/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()); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 775df230b6..e32a2e8086 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/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 alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List 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 keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); + PageData 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 alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); + PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List 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 keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); + PageData 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 alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); List 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 checkFunction) throws Exception { + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { await() .alias("data by query") .atMost(30, TimeUnit.SECONDS) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index 2f244807bd..610e27944e 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/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 diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index c6ad3c12d3..cc55e09fd5 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/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 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 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(); diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 153228a865..3c62333122 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/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) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index 09a363853f..ddefa966f0 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/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 caffeineBuilder - = Caffeine.newBuilder() + Caffeine caffeineBuilder = Caffeine.newBuilder() .weigher(collectionSafeWeigher()) .maximumWeight(cacheSpec.getMaxSize()) + .recordStats() .ticker(ticker()); if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java index 9a4e875154..7dcf7ffe66 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java +++ b/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(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java index 9bb549994b..073d9347fa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java +++ b/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(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java index e855f8efd3..0502a10105 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java +++ b/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; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java index e99849ea82..4c6df3825d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java +++ b/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 predicates; + public boolean hasPredicate(FilterPredicateType type) { + return containsPredicate(predicates, type); + } + + private boolean containsPredicate(List predicates, FilterPredicateType type) { + return predicates.stream().anyMatch(predicate -> { + if (predicate instanceof ComplexFilterPredicate complexPredicate) { + return containsPredicate(complexPredicate.getPredicates(), type); + } else { + return predicate.getType() == type; + } + }); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java index 8c27400961..b0afbcc7ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java +++ b/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)); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java index 50f73e887b..2562e19be4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java +++ b/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 diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java index af7c45ac5b..687b39932e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java +++ b/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 } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java index 58355c627d..ca4531c123 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java +++ b/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 diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java new file mode 100644 index 0000000000..82b366f6ae --- /dev/null +++ b/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 duration; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NO_DATA; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java index 65316eda88..4bc547695b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java +++ b/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 { @NotNull diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java index d36ba33849..6f3cdbdd3c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java +++ b/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); + } + } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 8909663771..555717ffc0 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -510,7 +510,7 @@ export const menuSectionMap = new Map([ MenuId.alarms, { id: MenuId.alarms, - name: 'alarm.alarms', + name: 'alarm.alarm-list', type: 'link', path: '/alarms/alarms', icon: 'mdi:alert-outline' diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html index 9e5036f6ff..97bd25836a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html @@ -27,8 +27,10 @@ - +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index b734757144..20bd3e1bde 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -130,30 +130,32 @@ {{ 'alarm-rule.advanced-settings' | translate }} -
- - {{ 'alarm-rule.propagate-alarm' | translate }} - +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + alarm-rule.alarm-rule-relation-types-list + + + {{key}} + close + + + + + + }
- @if (configFormGroup.get('propagate').value) { - - alarm-rule.alarm-rule-relation-types-list - - - {{key}} - close - - - - - - }
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html index a71874c13c..5deddffd87 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html @@ -66,7 +66,7 @@
-
alarm-rule.entity-type
+
alarm-rule.target-entity-type
{{ 'alarm-rule.any-type' | translate }} @@ -78,7 +78,7 @@
@if (alarmRuleFilterConfigForm.get('entityType').value) {
-
alarm-rule.alarm-rule-entity-list
+
alarm-rule.target-entities
{ this.columns.push(new EntityTableColumn('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('entityType', 'alarm-rule.entity-type', '15%', + this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.target-entity-type', '15%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.entity-name', '30%', + this.columns.push(new EntityLinkTableColumn('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('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('clearRule', 'alarm-rule.cleared', '70px', + this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '90px', entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); this.cellActionDescriptors.push( diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index 34f165420a..d67cd51a9a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -90,15 +90,47 @@
}
+ + @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+ + + {{ 'alarm-rule.filter-preview' | translate }} + + +
+ @if (specText) { + {{ specText }} + } + @if (conditionFormGroup.get('expression.filters').value?.length) { + + + } @else { + {{ 'alarm-rule.no-filter-preview' | translate }} + } +
+
+
+
+ } +
{{ 'alarm-rule.condition-settings' | translate }}
alarm-rule.condition-type - + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + @if (isNoData) { + alarm-rule.condition-type-hint + } @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
@@ -146,7 +178,7 @@
- +
@@ -162,9 +194,8 @@ @if (!readonly) { } @@ -186,6 +217,9 @@ } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { {{ defaultValuePatternError | translate }} } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } @@ -198,9 +232,14 @@ {{ argument }} } - - {{ 'calculated-fields.hint.argument-name-required' | translate }} - + @if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) { + + {{ 'calculated-fields.hint.argument-name-required' | translate }} + + } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index 2b0f25c2fe..f565a66168 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/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(false); ComplexOperation = ComplexOperation; - complexOperationTranslationMap = complexOperationTranslationMap; + complexOperationTranslationMap = filterOperationTranslationMap; + + specText = ''; + + filtersValid: boolean = false; functionArgs: Array; argumentsEditorCompleter: TbEditorCompleter; @@ -105,11 +112,14 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent; + isNoData: boolean = false; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, public dialogRef: MatDialogRef, - 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 { - 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) { + 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) { + 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 0) { + this.specText = this.specText + ':'; + } + } + cancel(): void { this.dialogRef.close(null); } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html index 0b722b4ec1..638075849f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -21,7 +21,7 @@
{{ 'alarm-rule.schedule-title' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 543b0a86dc..6f17aa8b59 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/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) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index 38e0bd8023..a9f4ad61a5 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -20,11 +20,11 @@ @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
-
+
alarm-rule.alarm-rule-additional-info
- +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss index b66573f417..6253aa9a4b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts index b1c7a4da7f..a4572629e0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts +++ b/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: diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html index 75bce37a80..c0c5a72d77 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -25,20 +25,25 @@
- - filter.operation.operation - - - {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} - - - - - +
+
+
+ {{ 'alarm-rule.filters' | translate }} +
+ + {{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }} + {{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }} + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts index 6961524bdd..3f86bdbe04 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts +++ b/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 { + 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
-
{{ filterControl.value?.argument }}
-
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}
-
+
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
+ @@ -62,14 +69,15 @@ }
} - - filter.no-key-filters - + @if (!filtersFormArray.length) { + + alarm-rule.no-filter + + + }
- -