diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 94b1a8b878..24b9b93eff 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -24,8 +24,11 @@ SET profile_data = jsonb_set( 'minAllowedScheduledUpdateIntervalInSecForCF', 60, 'maxRelationLevelPerCfArgument', 10, 'maxRelatedEntitiesToReturnPerCfArgument', 100, - 'minAllowedDeduplicationIntervalInSecForCF', 60, - 'minAllowedAggregationIntervalInSecForCF', 60 + 'minAllowedDeduplicationIntervalInSecForCF', 10, + 'minAllowedAggregationIntervalInSecForCF', 60, + 'intermediateAggregationIntervalInSecForCF', 300, + 'cfReevaluationCheckInterval', 60, + 'alarmsReevaluationInterval', 60 ) || jsonb_strip_nulls(profile_data -> 'configuration') @@ -36,7 +39,10 @@ WHERE NOT ( 'maxRelationLevelPerCfArgument', 'maxRelatedEntitiesToReturnPerCfArgument', 'minAllowedDeduplicationIntervalInSecForCF', - 'minAllowedAggregationIntervalInSecForCF' + 'minAllowedAggregationIntervalInSecForCF', + 'intermediateAggregationIntervalInSecForCF', + 'cfReevaluationCheckInterval', + 'alarmsReevaluationInterval' ] ); 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 654ae8bf29..9848ac2fe6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -666,14 +666,6 @@ public class ActorSystemContext { @Getter private long cfCalculationResultTimeout; - @Value("${actors.calculated_fields.check_interval:60}") - @Getter - private long cfCheckInterval; - - @Value("${actors.alarms.reevaluation_interval:60}") - @Getter - private long alarmRulesReevaluationInterval; - @Autowired @Getter private MqttClientSettings mqttClientSettings; 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 b19ad1a8b4..4f608d7186 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 @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; @@ -115,6 +116,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final TbQueueCalculatedFieldSettings cfSettings; protected final TenantId tenantId; + private long cfCheckInterval; + protected TbActorCtx ctx; CalculatedFieldManagerMessageProcessor(ActorSystemContext systemContext, TenantId tenantId) { @@ -144,10 +147,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); - if (cfsReevaluationTask != null) { - cfsReevaluationTask.cancel(true); - cfsReevaluationTask = null; - } + cancelReevaluationTask(); ctx.stop(ctx.getSelf()); } @@ -155,6 +155,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId()); initEntitiesCache(); initCalculatedFields(); + cfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); scheduleCfsReevaluation(); msg.getCallback().onSuccess(); } @@ -190,7 +191,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } catch (Exception e) { log.warn("[{}] Failed to trigger CFs reevaluation", tenantId, e); } - }, systemContext.getCfCheckInterval(), systemContext.getCfCheckInterval(), TimeUnit.SECONDS); + }, cfCheckInterval, cfCheckInterval, TimeUnit.SECONDS); } public void onEntityLifecycleMsg(CalculatedFieldEntityLifecycleMsg msg) throws CalculatedFieldException { @@ -257,10 +258,16 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) { + long updatedCfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); + if (cfCheckInterval != updatedCfCheckInterval) { + cfCheckInterval = updatedCfCheckInterval; + cancelReevaluationTask(); + scheduleCfsReevaluation(); + } Stream.concat( calculatedFields.values().stream(), entityIdCalculatedFields.values().stream().flatMap(Collection::stream) - ).forEach(CalculatedFieldCtx::updateTenantProfileProperties); + ).forEach(CalculatedFieldCtx::setTenantProfileProperties); callback.onSuccess(); } @@ -875,4 +882,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } + private void cancelReevaluationTask() { + if (cfsReevaluationTask != null) { + cfsReevaluationTask.cancel(true); + cfsReevaluationTask = null; + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 9c04cb92bd..126d426415 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -166,6 +166,7 @@ public class SystemInfoController extends BaseController { systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); + systemParams.setIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getIntermediateAggregationIntervalInSecForCF()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 19cc2341ad..f2c345b6d1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -166,9 +166,12 @@ public class TenantProfileController extends BaseController { " \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" + " \"maxDataPointsPerRollingArg\": 1000,\n" + " \"maxStateSizeInKBytes\": 32,\n" + - " \"maxSingleValueArgumentSizeInKBytes\": 2" + - " \"minAllowedDeduplicationIntervalInSecForCF\": 60" + - " \"minAllowedAggregationIntervalInSecForCF\": 60" + + " \"maxSingleValueArgumentSizeInKBytes\": 2," + + " \"minAllowedDeduplicationIntervalInSecForCF\": 10," + + " \"minAllowedAggregationIntervalInSecForCF\": 60," + + " \"intermediateAggregationIntervalInSecForCF\": 300," + + " \"cfReevaluationCheckInterval\": 60," + + " \"alarmsReevaluationInterval\": 60" + " }\n" + " },\n" + " \"default\": false\n" + 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 e7c4801c12..9d6727a7fa 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 @@ -239,7 +239,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { TenantProfile tenantProfile = tenantProfileCache.get(ctx.getTenantId()); return tenantProfile != null && tenantProfileId.equals(tenantProfile.getId()); }) - .forEach(CalculatedFieldCtx::updateTenantProfileProperties); + .forEach(CalculatedFieldCtx::setTenantProfileProperties); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 95a524187a..5173c48892 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -26,8 +26,6 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; import org.thingsboard.server.utils.CalculatedFieldUtils; import java.io.Closeable; @@ -41,7 +39,7 @@ import java.util.stream.Collectors; @Getter public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { - protected static final long DEFAULT_LAST_UPDATE_TS = -1L; + public static final long DEFAULT_LAST_UPDATE_TS = -1L; protected final EntityId entityId; protected CalculatedFieldCtx ctx; @@ -87,16 +85,15 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, if (existingEntry == null || newEntry.isForceResetPrevious()) { validateNewEntry(key, newEntry); - if (existingEntry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { - relatedEntitiesArgumentEntry.updateEntry(newEntry); - } else if (existingEntry instanceof EntityAggregationArgumentEntry entityAggArgumentEntry) { - entityAggArgumentEntry.updateEntry(newEntry); + if (existingEntry instanceof RelatedEntitiesArgumentEntry || + existingEntry instanceof EntityAggregationArgumentEntry) { + updateEntry(existingEntry, newEntry); } else { arguments.put(key, newEntry); } entryUpdated = true; } else { - entryUpdated = existingEntry.updateEntry(newEntry); + entryUpdated = updateEntry(existingEntry, newEntry); } if (entryUpdated) { @@ -104,7 +101,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, updatedArguments = new HashMap<>(argumentValues.size()); } updatedArguments.put(key, newEntry); - updateLastUpdateTimestamp(newEntry); } } @@ -116,6 +112,10 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, return updatedArguments; } + protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry) { + return existingEntry.updateEntry(newEntry); + } + @Override public void reset() { // must reset everything dependent on arguments requiredArguments = null; @@ -158,23 +158,29 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, return resultNode; } - private void updateLastUpdateTimestamp(ArgumentEntry entry) { - long newTs = this.latestTimestamp; - if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - newTs = singleValueArgumentEntry.getTs(); - } else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) { - Map.Entry lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry(); - newTs = (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS; - } else if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { - newTs = relatedEntitiesArgumentEntry.getEntityInputs().values().stream() - .mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : DEFAULT_LAST_UPDATE_TS) - .max() - .orElse(DEFAULT_LAST_UPDATE_TS); - } else if (entry instanceof GeofencingArgumentEntry geofencingArgumentEntry) { - newTs = geofencingArgumentEntry.getZoneStates().values().stream() - .mapToLong(GeofencingZoneState::getTs).max().orElse(DEFAULT_LAST_UPDATE_TS); + public long getLatestTimestamp() { + long latestTs = DEFAULT_LAST_UPDATE_TS; + + boolean allDefault = arguments.values().stream().allMatch(entry -> { + if (entry instanceof SingleValueArgumentEntry single) { + return single.isDefaultValue(); + } + return false; + }); + + for (ArgumentEntry entry : arguments.values()) { + if (entry instanceof SingleValueArgumentEntry single) { + if (allDefault) { + latestTs = Math.max(latestTs, single.getTs()); + } else if (!single.isDefaultValue()) { + latestTs = Math.max(latestTs, single.getTs()); + } + } else if (entry instanceof HasLatestTs hasLatestTsEntry) { + latestTs = Math.max(latestTs, hasLatestTsEntry.getLatestTs()); + } } - this.latestTimestamp = Math.max(this.latestTimestamp, newTs); + + return latestTs; } protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { 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 c2c65d3327..7969b2ce30 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 @@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.util.TimeUtils; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; @@ -119,6 +120,7 @@ public class CalculatedFieldCtx implements Closeable { private long maxStateSize; private long maxSingleValueArgumentSize; + private long intermediateAggregationIntervalMillis; private boolean relationQueryDynamicArguments; private List mainEntityGeofencingArgumentNames; @@ -126,6 +128,8 @@ public class CalculatedFieldCtx implements Closeable { private List relatedEntityArgumentNames; private long scheduledUpdateIntervalMillis; + private long cfCheckReevaluationInterval; + private long alarmReevaluationInterval; private Argument propagationArgument; private boolean applyExpressionForResolvedArguments; @@ -212,8 +216,7 @@ public class CalculatedFieldCtx implements Closeable { this.alarmService = systemContext.getAlarmService(); this.cfProcessingService = systemContext.getCalculatedFieldProcessingService(); - this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + setTenantProfileProperties(); } public boolean requiresScheduledReevaluation() { @@ -227,6 +230,12 @@ public class CalculatedFieldCtx implements Closeable { lastReevaluationTs = now; return true; } + if (entityAggregationConfig.isProduceIntermediateResult()) { + if (now - lastReevaluationTs >= intermediateAggregationIntervalMillis) { + lastReevaluationTs = now; + return true; + } + } ZonedDateTime lastReevaluationTime = TimeUtils.toZonedDateTime(lastReevaluationTs, entityAggregationConfig.getInterval().getZoneId()); long previousIntervalEndTs = entityAggregationConfig.getInterval().getDateTimeIntervalEndTs(lastReevaluationTime); if (now >= previousIntervalEndTs) { @@ -237,7 +246,7 @@ public class CalculatedFieldCtx implements Closeable { boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { if (requiresScheduledReevaluation) { - long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); + long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(alarmReevaluationInterval); if (now - lastReevaluationTs >= reevaluationIntervalMillis) { lastReevaluationTs = now; return true; @@ -292,9 +301,13 @@ public class CalculatedFieldCtx implements Closeable { } } - public void updateTenantProfileProperties() { - this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + public void setTenantProfileProperties() { + ApiLimitService apiLimitService = systemContext.getApiLimitService(); + this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; + this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getIntermediateAggregationIntervalInSecForCF)); + this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); + this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); } public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { @@ -664,7 +677,8 @@ public class CalculatedFieldCtx implements Closeable { && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics()); boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark()); - return metricsChanged || watermarkChanged; + boolean produceIntermediateResultChanged = thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult(); + return metricsChanged || watermarkChanged || produceIntermediateResultChanged; } return false; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java new file mode 100644 index 0000000000..672591fcba --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state; + +public interface HasLatestTs { + + long getLatestTs(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 97916192b5..4d0c4d7724 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -162,9 +162,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry { public boolean updateEntry(ArgumentEntry entry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) { if (singleValueEntry.getTs() < this.ts) { - if (!isDefaultValue()) { - return false; - } + return false; } Long newVersion = singleValueEntry.getVersion(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index b5a680a072..66d2807e6c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -31,11 +31,13 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + @Data @NoArgsConstructor @AllArgsConstructor @Slf4j -public class TsRollingArgumentEntry implements ArgumentEntry { +public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs { private Integer limit; private Long timeWindow; @@ -83,6 +85,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry { return tsRecords; } + public long getLatestTs() { + var lastEntry = tsRecords.lastEntry(); + return (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS; + } + @Override public TbelCfArg toTbelCfArg() { List values = new ArrayList<>(tsRecords.size()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java index 2abe78d243..219cf471ed 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java @@ -23,14 +23,17 @@ import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.Map; import java.util.stream.Collectors; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + @Data @AllArgsConstructor -public class RelatedEntitiesArgumentEntry implements ArgumentEntry { +public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs { private final Map entityInputs; @@ -46,6 +49,19 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry { return entityInputs; } + @Override + public long getLatestTs() { + long latestTs = DEFAULT_LAST_UPDATE_TS; + for (ArgumentEntry entry : entityInputs.values()) { + if (entry instanceof SingleValueArgumentEntry single) { + if (!single.isDefaultValue()) { + latestTs = Math.max(latestTs, single.getTs()); + } + } + } + return latestTs; + } + @Override public boolean updateEntry(ArgumentEntry entry) { if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index ba0b151eb2..721e93c6fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -60,9 +60,10 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt private AggInterval interval; private long watermarkDuration; - private long checkInterval; private Map metrics; + private boolean produceIntermediateResult; + private EntityAggregationDebugArgumentsTracker debugTracker; private CalculatedFieldProcessingService cfProcessingService; @@ -78,9 +79,9 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt var configuration = (EntityAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); Watermark watermark = configuration.getWatermark(); watermarkDuration = watermark == null ? 0 : TimeUnit.SECONDS.toMillis(watermark.getDuration()); - checkInterval = TimeUnit.SECONDS.toMillis(ctx.getSystemContext().getCfCheckInterval()); interval = configuration.getInterval(); metrics = configuration.getMetrics(); + produceIntermediateResult = configuration.isProduceIntermediateResult(); } @Override @@ -204,10 +205,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt handleExpiredInterval(intervalEntry, args, results); expiredIntervals.add(intervalEntry); } else if (now - startTs >= intervalEntry.getIntervalDuration()) { - handleActiveInterval(intervalEntry, args, results); + handleActiveInterval(ctx.getCfCheckReevaluationInterval(), intervalEntry, args, results); if (watermarkDuration == 0) { expiredIntervals.add(intervalEntry); } + } else if (produceIntermediateResult) { + handleActiveInterval(ctx.getIntermediateAggregationIntervalMillis(), intervalEntry, args, results); } } @@ -225,11 +228,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt }); } - private void handleActiveInterval(AggIntervalEntry intervalEntry, + private void handleActiveInterval(long cfCheckInterval, + AggIntervalEntry intervalEntry, Map args, Map> results) { args.forEach((argName, argEntryIntervalStatus) -> { - if (argEntryIntervalStatus.intervalPassed(checkInterval)) { + if (argEntryIntervalStatus.intervalPassed(cfCheckInterval)) { if (argEntryIntervalStatus.argsUpdated()) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastArgsRefreshTs(DEFAULT_LAST_UPDATE_TS); 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 bd2c272bca..18629bd370 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 @@ -225,6 +225,20 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { .build()); } + @Override + protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry) { + if (!(existingArgumentEntry instanceof SingleValueArgumentEntry existingEntry) || + !(newArgumentEntry instanceof SingleValueArgumentEntry newEntry)) { + return super.updateEntry(existingArgumentEntry, newArgumentEntry); + } + if (newEntry.getTs() < existingEntry.getTs()) { + if (existingEntry.isDefaultValue()) { + existingEntry.setTs(newEntry.getTs()); + } + } + return super.updateEntry(existingEntry, newEntry); + } + public void processAlarmAction(Alarm alarm, ActionType action) { switch (action) { case ALARM_ACK -> processAlarmAck(alarm); 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 c6a5cbf418..569bbe9310 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 @@ -157,7 +157,7 @@ public class AlarmRuleState { private AlarmEvalResult evalDuration(CalculatedFieldCtx ctx) { if (eval(condition.getExpression(), ctx)) { long ts = System.currentTimeMillis(); - if (firstEventTs == 0) { + if (firstEventTs <= 0) { firstEventTs = state.getLatestTimestamp(); } lastCheckTs = ts; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index bcc4d3ffcd..01c7119993 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -25,13 +25,16 @@ import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import java.util.Map; import java.util.stream.Collectors; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + @Data @Slf4j -public class GeofencingArgumentEntry implements ArgumentEntry { +public class GeofencingArgumentEntry implements ArgumentEntry, HasLatestTs { private Map zoneStates; @@ -58,6 +61,12 @@ public class GeofencingArgumentEntry implements ArgumentEntry { return zoneStates; } + @Override + public long getLatestTs() { + return zoneStates.values().stream() + .mapToLong(GeofencingZoneState::getTs).max().orElse(DEFAULT_LAST_UPDATE_TS); + } + @Override public boolean updateEntry(ArgumentEntry entry) { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java index 74bbdd8a34..7e0701cd2d 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -104,7 +104,6 @@ public class CalculatedFieldArgumentUtils { public static TsKvEntry createDefaultTsKvEntry(Argument argument, long ts) { return new BasicTsKvEntry(ts, createDefaultKvEntry(argument), DEFAULT_VERSION); } - public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) { return new BaseAttributeKvEntry(createDefaultKvEntry(argument), ts, DEFAULT_VERSION); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d3d04bcea0..66669a8280 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -538,11 +538,6 @@ actors: configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" # Time in seconds to receive calculation result. calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}" - # Interval in seconds to check calculated fields for re-evaluation interval. 1 minute by default. - 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: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 5f91dab190..15567191ff 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; 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.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbAlarmResult; @@ -86,10 +85,6 @@ import static org.testcontainers.shaded.org.awaitility.Awaitility.await; @Slf4j @DaoSqlTest -@TestPropertySource(properties = { - "actors.calculated_fields.check_interval=1", - "actors.alarms.reevaluation_interval=1" -}) public class AlarmRulesTest extends AbstractControllerTest { @MockitoSpyBean @@ -105,6 +100,13 @@ public class AlarmRulesTest extends AbstractControllerTest { @Before public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setCfReevaluationCheckInterval(1); + tenantProfileConfig.setAlarmsReevaluationInterval(1); + }); + loginTenantAdmin(); device = createDevice("Device A", "aaa"); deviceId = device.getId(); @@ -194,6 +196,30 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + @Test + public void testCreateAlarm_eventBeforeDefaultTs() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"values\": {\"temperature\": 50}, \"ts\": " + (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30) + "}")); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + @Test public void testCreateAlarm_repeatingCondition() throws Exception { Argument temperatureArgument = new Argument(); @@ -350,6 +376,32 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + @Test + public void testCreateAlarm_durationCondition_defaultValue() { + Argument powerConsumptionArgument = new Argument(); + powerConsumptionArgument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + powerConsumptionArgument.setDefaultValue("3500"); + Map arguments = Map.of( + "powerConsumption", powerConsumptionArgument + ); + + long createDurationMs = 2000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, null, + new AlarmConditionValue(2000L, null), null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + arguments, createRules, null); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + } + @Test public void testCreateAlarm_currentOwnerArgument() throws Exception { Argument temperatureArgument = new Argument(); diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index af0c1b9909..6f1ae6f481 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -580,6 +580,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); + long ts = System.currentTimeMillis() - 300000L; postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)); @@ -614,6 +615,90 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testSimpleCalculatedFieldWhenUseLatestTsIsTrueAndDefaultArguments() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("a + b + c"); + calculatedField.setDebugSettings(DebugSettings.all()); + calculatedField.setConfigurationVersion(1); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument1 = new Argument(); + ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null); + argument1.setRefEntityKey(refEntityKey1); + argument1.setDefaultValue("100"); + Argument argument2 = new Argument(); + ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("b", ArgumentType.TS_LATEST, null); + argument2.setRefEntityKey(refEntityKey2); + argument2.setDefaultValue("200"); + Argument argument3 = new Argument(); + ReferencedEntityKey refEntityKey3 = new ReferencedEntityKey("c", ArgumentType.TS_LATEST, null); + argument3.setRefEntityKey(refEntityKey3); + argument3.setDefaultValue("300"); + config.setArguments(Map.of("a", argument1, "b", argument2, "c", argument3)); + config.setExpression("a + b + c"); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("d"); + output.setDecimalsByDefault(0); + config.setOutput(output); + + config.setUseLatestTs(true); + + calculatedField.setConfiguration(config); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation with default arguments").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode d = getLatestTelemetry(testDevice.getId(), "d"); + assertThat(d).isNotNull(); + assertThat(d.get("d").get(0).get("value").asText()).isEqualTo("600"); + }); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}")); + + await().alias("update telemetry -> save result with ts of 'a' argument").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d", "a"); + assertThat(keys).isNotNull(); + String aTs = keys.get("a").get(0).get("ts").asText(); + assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(aTs); + assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("510"); + }); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":20}")); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"c\":30}")); + + await().alias("update telemetry -> save result with latest ts of updated arguments").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d"); + assertThat(keys).isNotNull(); + assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("60"); + }); + + String latestTs = getLatestTelemetry(testDevice.getId(), "d").get("d").get(0).get("ts").asText(); + + doDelete("/api/plugins/telemetry/DEVICE/" + testDevice.getId() + "/timeseries/delete?keys=b&deleteAllDataForKeys=true").andExpect(status().isOk()); + + await().alias("delete telemetry -> save result with previous latest ts and default argument").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d"); + assertThat(keys).isNotNull(); + assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(latestTs); + assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("240"); + }); + } + @Test public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java index 3044525757..253d02ebe5 100644 --- a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -20,7 +20,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -54,9 +53,6 @@ import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTE @DaoSqlTest @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -@TestPropertySource(properties = { - "actors.calculated_fields.check_interval=1" -}) public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest { private Tenant savedTenant; @@ -68,6 +64,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest updateDefaultTenantProfileConfig(tenantProfileConfig -> { tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); tenantProfileConfig.setMinAllowedAggregationIntervalInSecForCF(1); + tenantProfileConfig.setCfReevaluationCheckInterval(1); }); Tenant tenant = new Tenant(); @@ -97,9 +94,8 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); - long intervalEndTs = customInterval.getCurrentIntervalEndTs(); + createConsumptionCF(device.getId(), customInterval, null); - CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, null); long interval = customInterval.getCurrentIntervalDurationMillis(); await().alias("create CF and no telemetry during interval -> save metric with default value") @@ -118,8 +114,9 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + createConsumptionCF(device.getId(), customInterval, null); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); - long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long tsBeforeInterval = currentIntervalStartTs - 1000; long tsInInterval_1 = currentIntervalStartTs + 1000; @@ -131,7 +128,6 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); long interval = customInterval.getCurrentIntervalDurationMillis(); - CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, null); await().alias("create CF -> perform aggregation after interval end") .atMost(2 * interval, TimeUnit.MILLISECONDS) @@ -161,8 +157,10 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + Watermark watermark = new Watermark(10); + createConsumptionCF(device.getId(), customInterval, watermark); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); - long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long tsBeforeInterval = currentIntervalStartTs - 1000L; long tsInInterval_1 = currentIntervalStartTs + 1000L; @@ -174,8 +172,6 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); long interval = customInterval.getCurrentIntervalDurationMillis(); - Watermark watermark = new Watermark(10); - CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, watermark); await().alias("create CF -> perform aggregation after interval end") .atMost(2 * interval, TimeUnit.MILLISECONDS) @@ -235,27 +231,21 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + createCFWith2Args(device.getId(), customInterval, null); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); - long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long tsBeforeInterval = currentIntervalStartTs - 1000; long tsInInterval_1 = currentIntervalStartTs + 1000; long tsInInterval_2 = currentIntervalStartTs + 500; long tsInInterval_3 = currentIntervalStartTs + 200; - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); - - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":43}}", tsBeforeInterval)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":39}}", tsInInterval_1)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":27}}", tsInInterval_2)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":50}}", tsInInterval_3)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120, \"temperature\":43}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100, \"temperature\":39}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180, \"temperature\":27}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120, \"temperature\":50}}", tsInInterval_3)); long interval = customInterval.getCurrentIntervalDurationMillis(); - CalculatedField consumptionCF = createCFWith2Args(device.getId(), customInterval, null); - await().alias("create CF -> perform aggregation after interval end") .atMost(2 * interval, TimeUnit.MILLISECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 0fa9b2dd78..40bfa668d2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -42,5 +42,6 @@ public class SystemParams { int maxRelationLevelPerCfArgument; long minAllowedDeduplicationIntervalInSecForCF; long minAllowedAggregationIntervalInSecForCF; + long intermediateAggregationIntervalInSecForCF; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java index f6095d41a7..488db86870 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java @@ -43,6 +43,7 @@ public class EntityAggregationCalculatedFieldConfiguration implements ArgumentsB private AggInterval interval; @Valid private Watermark watermark; + private boolean produceIntermediateResult; @Valid @NotNull private Output output; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 87fa4a85da..0795293fa3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -190,10 +190,19 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxStateSizeInKBytes = 32; @Schema(example = "2") private long maxSingleValueArgumentSizeInKBytes = 2; - @Schema(example = "60") - private long minAllowedDeduplicationIntervalInSecForCF = 60; + @Schema(example = "10") + private long minAllowedDeduplicationIntervalInSecForCF = 10; @Schema(example = "60") private long minAllowedAggregationIntervalInSecForCF = 60; + @Builder.Default + @Schema(example = "300") + private long intermediateAggregationIntervalInSecForCF = 300; + @Builder.Default + @Schema(example = "60") + private long cfReevaluationCheckInterval = 60; + @Builder.Default + @Schema(example = "60") + private long alarmsReevaluationInterval = 60; @Override public long getProfileThreshold(ApiUsageRecordKey key) { @@ -249,4 +258,16 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura return maxRuleNodeExecutionsPerMessage; } + public long getCfReevaluationCheckInterval() { + return cfReevaluationCheckInterval <= 0 ? 60 : cfReevaluationCheckInterval; + } + + public long getAlarmsReevaluationInterval() { + return alarmsReevaluationInterval <= 0 ? 60 : alarmsReevaluationInterval; + } + + public long getIntermediateAggregationIntervalInSecForCF() { + return intermediateAggregationIntervalInSecForCF <= 0 ? 300 : intermediateAggregationIntervalInSecForCF; + } + } diff --git a/common/queue/pom.xml b/common/queue/pom.xml index df592338ef..fca9dc59ce 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -69,7 +69,7 @@ kafka-clients - org.lz4 + at.yawk.lz4 lz4-java diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index c10da4e6c6..6c333f36ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -50,7 +50,7 @@ public class CalculatedFieldDataValidator extends DataValidator validateCalculatedFieldConfiguration(calculatedField); validateSchedulingConfiguration(tenantId, calculatedField); validateRelationQuerySourceArguments(tenantId, calculatedField); - validateAggregationConfiguration(tenantId, calculatedField); + validateRelatedAggregationConfiguration(tenantId, calculatedField); validateEntityAggregationConfiguration(tenantId, calculatedField); } @@ -119,7 +119,7 @@ public class CalculatedFieldDataValidator extends DataValidator wrapAsDataValidation(() -> relationQueryDynamicSourceConfiguration.validateMaxRelationLevel(argumentName, maxRelationLevel))); } - private void validateAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { + private void validateRelatedAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { if (!(calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfiguration)) { return; } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 636c80d5b3..fb41cdf77f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -15,10 +15,11 @@ */ package org.thingsboard.server.msa.connectivity; +import com.google.gson.JsonObject; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; @@ -30,26 +31,83 @@ import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rest.client.RestClient; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestConfig; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.HasSubject; +import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.TestProperties; import javax.net.ssl.SSLContext; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.EMAIL; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MICROSOFT_TEAMS; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; public class JavaRestClientTest extends AbstractContainerTest { + public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; + public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; private RestClient restClient; + private Tenant tenant; + private User user; @BeforeClass public void beforeClass() throws Exception { @@ -77,11 +135,25 @@ public class JavaRestClientTest extends AbstractContainerTest { @BeforeMethod public void setUp() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + // create tenant and tenant admin + tenant = new Tenant(); + tenant.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.randomAlphabetic(5)); + tenant = restClient.saveTenant(tenant); + + String email = RandomStringUtils.randomAlphabetic(5) + "@gmail.com"; + user = restClient.saveUser(defaultTenantAdmin(tenant.getId(), email), false); + restClient.activateUser(user.getId(), "password123", false); + restClient.login(email, "password123"); } @AfterMethod public void tearDown() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + if (tenant != null) { + restClient.deleteTenant(tenant.getId()); + } } @Test @@ -123,6 +195,219 @@ public class JavaRestClientTest extends AbstractContainerTest { PageData allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null); assertThat(allClearedAlarms.getData()).hasSize(0); + } + + @Test + public void testTimeSeriesByReadTsKvQueries() { + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); + assertThat(device).isNotNull(); + + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + for (int i = 0; i < 3; i++) { + JsonObject values = new JsonObject(); + values.addProperty("temperature", i + 25); + testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), JacksonUtil.toJsonNode(createPayload().toString())); + } + + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 25, \"humidity\": 60}")); + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 27, \"humidity\": 59}")); + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 33, \"humidity\": 62}")); + + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(EntityType.DEVICE); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + + EntityDataQuery entityDataQuery = new EntityDataQuery(filter, pageLink, entityFields, null, null); + AvailableEntityKeys availableEntityKeys = restClient.findAvailableEntityKeysByQuery(entityDataQuery, true, true, null); + assertThat(availableEntityKeys).isNotNull(); + assertThat(availableEntityKeys.timeseries()).contains("temperature", "humidity"); + } + + @Test + public void testFindNotifications() { + NotificationTarget notificationTarget = createNotificationTarget(user.getId()); + String notificationText1 = "Notification 1"; + NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText1, new NotificationDeliveryMethod[]{WEB}); + NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationTemplate.getId()); + + String notificationText2 = "Notification 2"; + NotificationTemplate notificationTemplate2 = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText2, new NotificationDeliveryMethod[]{WEB}); + NotificationRequest notificationRequest2 = submitNotificationRequest(notificationTarget.getId(), notificationTemplate2.getId()); + + PageData initialRequests = restClient.getNotificationRequests(new PageLink(30)); + assertThat(initialRequests.getTotalElements()).isGreaterThanOrEqualTo(2); + + NotificationRequestInfo notificationRequestInfo = restClient.getNotificationRequestById(notificationRequest.getId()).get(); + assertThat(notificationRequestInfo.getName()).isEqualTo(notificationRequest.getName()); + assertThat(notificationRequestInfo.getTemplateName()).isEqualTo(notificationTemplate.getName()); + + NotificationRequestPreview requestPreview = restClient.getNotificationRequestPreview(notificationRequest, 10); + assertThat(requestPreview.getTotalRecipientsCount()).isEqualTo(1); + assertThat(requestPreview.getRecipientsPreview()).isEqualTo(List.of(user.getEmail())); + + PageData notifications = restClient.getNotifications(false, WEB, new PageLink(30)); + assertThat(notifications.getTotalElements()).isEqualTo(2); + + Integer unreadCount = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCount).isEqualTo(2); + + restClient.markNotificationAsRead(notifications.getData().get(0).getId()); + + Integer unreadCountAfterRead = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCountAfterRead).isEqualTo(1); + + restClient.markAllNotificationsAsRead(WEB); + + Integer unreadCountAfterAllRead = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCountAfterAllRead).isEqualTo(0); + + restClient.deleteNotification(notifications.getData().get(0).getId()); + notifications = restClient.getNotifications(false, WEB, new PageLink(30)); + assertThat(notifications.getTotalElements()).isEqualTo(1); + + restClient.deleteNotificationRequest(notificationRequest.getId()); + PageData requestsAfterUpdate = restClient.getNotificationRequests(new PageLink(30)); + assertThat(requestsAfterUpdate.getTotalElements()).isEqualTo(initialRequests.getTotalElements() - 1); + List availableDeliveryMethods = restClient.getAvailableDeliveryMethods(); + assertThat(availableDeliveryMethods).contains(WEB, EMAIL, MICROSOFT_TEAMS); + } + + @Test + public void testSaveNotificationSettings() { + NotificationSettings settings = new NotificationSettings(); + SlackNotificationDeliveryMethodConfig slackConfig = new SlackNotificationDeliveryMethodConfig(); + String slackToken = "xoxb-123123123"; + slackConfig.setBotToken(slackToken); + settings.setDeliveryMethodsConfigs(Map.of( + NotificationDeliveryMethod.SLACK, slackConfig + )); + + restClient.saveNotificationSettings(settings); + + NotificationSettings savedSettings = restClient.getNotificationSettings().get(); + assertThat(savedSettings.getDeliveryMethodsConfigs()).hasSize(1); + assertThat(savedSettings.getDeliveryMethodsConfigs().get(slackConfig.getMethod())).isEqualTo(slackConfig); + + // save user notification settings + var entityActionNotificationPref = new UserNotificationSettings.NotificationPref(); + entityActionNotificationPref.setEnabled(true); + entityActionNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + UserNotificationSettings userNotificationSettings = new UserNotificationSettings(Map.of( + NotificationType.ENTITY_ACTION, entityActionNotificationPref + )); + UserNotificationSettings saved = restClient.saveUserNotificationSettings(userNotificationSettings); + UserNotificationSettings retrieved = restClient.getUserNotificationSettings().get(); + assertThat(retrieved).isEqualTo(saved); + } + + @Test + public void testSaveDomain() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + Domain domain = new Domain(); + String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + domain.setName(prefix + ".test.com"); + Domain savedDomain = restClient.saveDomain(domain); + assertThat(savedDomain.getName()).isEqualTo(domain.getName()); + + PageData domainInfos = restClient.getTenantDomainInfos(new PageLink(10, 0 , prefix)); + assertThat(domainInfos.getData()).hasSize(1); + } + + @Test + public void testSaveMobileApp() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + MobileApp mobileApp = new MobileApp(); + String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + mobileApp.setPkgName(prefix + "test.app.apple"); + mobileApp.setPlatformType(PlatformType.ANDROID); + mobileApp.setAppSecret(RandomStringUtils.randomAlphabetic(20)); + mobileApp.setStatus(MobileAppStatus.DRAFT); + + MobileApp savedMobileApp = restClient.saveMobileApp(mobileApp); + assertThat(savedMobileApp.getName()).isEqualTo(mobileApp.getName()); + + PageData retrieved = restClient.getTenantMobileApps(new PageLink(10, 0, prefix)); + assertThat(retrieved.getData()).hasSize(1); + + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + String bundlePrefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + mobileAppBundle.setTitle(bundlePrefix + "Test Bundle"); + mobileAppBundle.setAndroidAppId(savedMobileApp.getId()); + + MobileAppBundle savedMobileAppBundle = restClient.saveMobileBundle(mobileAppBundle); + PageData bundleInfos = restClient.getTenantMobileBundleInfos(new PageLink(10, 0, bundlePrefix)); + assertThat(bundleInfos.getData()).hasSize(1); + } + + private NotificationTarget createNotificationTarget(UserId... usersIds) { + UserListFilter filter = new UserListFilter(); + filter.setUsersIds(Arrays.stream(usersIds).map(UUIDBased::getId).toList()); + + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setName(filter.toString() + org.apache.commons.lang3.RandomStringUtils.randomNumeric(5)); + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + targetConfig.setUsersFilter(filter); + notificationTarget.setConfiguration(targetConfig); + return restClient.saveNotificationTarget(notificationTarget); + } + + private NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, + String text, NotificationDeliveryMethod... deliveryMethods) { + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setName("Notification template: " + RandomStringUtils.randomAlphabetic(5)); + notificationTemplate.setNotificationType(notificationType); + NotificationTemplateConfig config = new NotificationTemplateConfig(); + config.setDeliveryMethodsTemplates(new HashMap<>()); + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; + switch (deliveryMethod) { + case WEB: { + deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); + break; + } + case EMAIL: { + deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); + break; + } + case SMS: { + deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + break; + } + case MOBILE_APP: + deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate(); + break; + default: + throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); + } + deliveryMethodNotificationTemplate.setEnabled(true); + deliveryMethodNotificationTemplate.setBody(text); + if (deliveryMethodNotificationTemplate instanceof HasSubject) { + ((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject); + } + config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); + } + notificationTemplate.setConfiguration(config); + return restClient.saveNotificationTemplate(notificationTemplate); + } + + private NotificationRequest submitNotificationRequest(NotificationTargetId targetId, NotificationTemplateId notificationTemplateId) { + NotificationRequestConfig config = new NotificationRequestConfig(); + config.setSendingDelayInSec(0); + NotificationRequest notificationRequest = NotificationRequest.builder() + .targets(List.of(targetId).stream().map(UUIDBased::getId).collect(Collectors.toList())) + .templateId(notificationTemplateId) + .additionalConfig(config) + .build(); + return restClient.saveNotificationRequest(notificationRequest); } } diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index fe56867dbe..df1d327c1b 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -15,7 +15,7 @@ "dependencies": { "config": "^4.1.1", "express": "^5.1.0", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "kafkajs": "^2.2.4", "long": "^5.3.2", "uuid-parse": "^1.1.0", diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock index 25ed229141..1d42a9cd96 100644 --- a/msa/js-executor/yarn.lock +++ b/msa/js-executor/yarn.lock @@ -945,10 +945,10 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -1533,9 +1533,9 @@ supports-preserve-symlinks-flag@^1.0.0: integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" - integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 1eacf42683..0c22a38465 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -19,7 +19,7 @@ "express": "^5.1.0", "http": "0.0.0", "http-proxy": "^1.18.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock index 7af758435e..0f8b6f3eed 100644 --- a/msa/web-ui/yarn.lock +++ b/msa/web-ui/yarn.lock @@ -1017,10 +1017,10 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -1615,9 +1615,9 @@ supports-preserve-symlinks-flag@^1.0.0: integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" - integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" diff --git a/pom.xml b/pom.xml index 38cb432a02..7966bb1253 100755 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ org.apache.kafka.common.network.NetworkReceive class in the application module. It addresses the issue https://issues.apache.org/jira/browse/KAFKA-4090. Here is the source to track https://github.com/apache/kafka/tree/trunk/clients/src/main/java/org/apache/kafka/common/network --> 3.9.1 - 1.8.1 + 1.10.1 8.10.1 3.5.3 2.2 @@ -102,7 +102,7 @@ 1.6.4 1.6.1 - 1.9.4 + 1.11.0 4.4 1.0.4TB 3.7.1 @@ -147,7 +147,7 @@ 9.2.0 1.1.10.5 9.10.0 - 4.1.125.Final + 4.1.128.Final @@ -1162,7 +1162,7 @@ - org.lz4 + at.yawk.lz4 lz4-java ${lz4.version} @@ -1445,6 +1445,12 @@ org.apache.cassandra cassandra-all ${cassandra-all.version} + + + org.lz4 + lz4-java + + org.testng diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 6eb61b3e13..c82a80e7cc 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -146,6 +146,8 @@ import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestPreview; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; @@ -2294,7 +2296,8 @@ public class RestClient implements Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getDomainInfoById(DomainId domainId) { @@ -2330,7 +2333,8 @@ public class RestClient implements Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getMobileAppById(MobileAppId mobileAppId) { @@ -2362,7 +2366,8 @@ public class RestClient implements Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getMobileBundleById(MobileAppBundleId mobileAppBundleId) { @@ -4289,11 +4294,23 @@ public class RestClient implements Closeable { } } - public PageData getNotifications(PageLink pageLink) { + public PageData getNotifications(Boolean unreadOnly, NotificationDeliveryMethod deliveryMethod, PageLink pageLink) { Map params = new HashMap<>(); + + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(baseURL).append("/api/notifications?").append(getUrlParams(pageLink)); addPageLinkToParam(params, pageLink); - return restTemplate.exchange( - baseURL + "/api/notifications?" + getUrlParams(pageLink), + + if (unreadOnly != null) { + urlBuilder.append("&unreadOnly={unreadOnly}"); + params.put("unreadOnly", unreadOnly.toString()); + } + if (deliveryMethod != null) { + urlBuilder.append("&deliveryMethod={deliveryMethod}"); + params.put("deliveryMethod", deliveryMethod.name()); + } + + return restTemplate.exchange(urlBuilder.toString(), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -4334,7 +4351,8 @@ public class RestClient implements Closeable { baseURL + uri, HttpMethod.PUT, HttpEntity.EMPTY, - Void.class); + Void.class, + params); } @@ -4342,7 +4360,7 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId()); } - public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) { + public NotificationRequest saveNotificationRequest(NotificationRequest notificationRequest) { return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody(); } @@ -4421,6 +4439,14 @@ public class RestClient implements Closeable { } } + public NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) { + return restTemplate.postForEntity(baseURL + "/api/notification/target", notificationTarget, NotificationTarget.class).getBody(); + } + + public NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) { + return restTemplate.postForEntity(baseURL + "/api/notification/template", notificationTemplate, NotificationTemplate.class).getBody(); + } + public AiModel saveAiModel(AiModel aiModel) { return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody(); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 954a6c7ef1..9824974311 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -97,7 +97,7 @@ kafka-clients - org.lz4 + at.yawk.lz4 lz4-java diff --git a/tools/pom.xml b/tools/pom.xml index 0b63d84b0a..f691913c43 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -55,6 +55,10 @@ org.apache.cassandra cassandra-all + + at.yawk.lz4 + lz4-java + commons-io commons-io diff --git a/ui-ngx/patches/@angular+common+18.2.13.patch b/ui-ngx/patches/@angular+common+18.2.13.patch new file mode 100644 index 0000000000..984562138a --- /dev/null +++ b/ui-ngx/patches/@angular+common+18.2.13.patch @@ -0,0 +1,66 @@ +diff --git a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs +index da69c17..d17f6ad 100755 +--- a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs ++++ b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs +@@ -19,6 +19,10 @@ export const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME + providedIn: 'root', + factory: () => XSRF_DEFAULT_HEADER_NAME, + }); ++/** ++ * Regex to match absolute URLs, including protocol-relative URLs. ++ */ ++const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i; + /** + * Retrieves the current XSRF token to use with the next outgoing request. + * +@@ -69,7 +73,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo + args: [XSRF_COOKIE_NAME] + }] }] }); + export function xsrfInterceptorFn(req, next) { +- const lcUrl = req.url.toLowerCase(); + // Skip both non-mutating requests and absolute URLs. + // Non-mutating requests don't require a token, and absolute URLs require special handling + // anyway as the cookie set +@@ -77,8 +80,7 @@ export function xsrfInterceptorFn(req, next) { + if (!inject(XSRF_ENABLED) || + req.method === 'GET' || + req.method === 'HEAD' || +- lcUrl.startsWith('http://') || +- lcUrl.startsWith('https://')) { ++ ABSOLUTE_URL_REGEX.test(req.url)) { + return next(req); + } + const token = inject(HttpXsrfTokenExtractor).getToken(); +diff --git a/node_modules/@angular/common/fesm2022/http.mjs b/node_modules/@angular/common/fesm2022/http.mjs +index 1655480..d1dbb38 100755 +--- a/node_modules/@angular/common/fesm2022/http.mjs ++++ b/node_modules/@angular/common/fesm2022/http.mjs +@@ -2352,6 +2352,10 @@ const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME' : '', + providedIn: 'root', + factory: () => XSRF_DEFAULT_HEADER_NAME, + }); ++/** ++ * Regex to match absolute URLs, including protocol-relative URLs. ++ */ ++const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i; + /** + * Retrieves the current XSRF token to use with the next outgoing request. + * +@@ -2402,7 +2406,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo + args: [XSRF_COOKIE_NAME] + }] }] }); + function xsrfInterceptorFn(req, next) { +- const lcUrl = req.url.toLowerCase(); + // Skip both non-mutating requests and absolute URLs. + // Non-mutating requests don't require a token, and absolute URLs require special handling + // anyway as the cookie set +@@ -2410,8 +2413,7 @@ function xsrfInterceptorFn(req, next) { + if (!inject(XSRF_ENABLED) || + req.method === 'GET' || + req.method === 'HEAD' || +- lcUrl.startsWith('http://') || +- lcUrl.startsWith('https://')) { ++ ABSOLUTE_URL_REGEX.test(req.url)) { + return next(req); + } + const token = inject(HttpXsrfTokenExtractor).getToken(); diff --git a/ui-ngx/patches/@angular+compiler+18.2.13.patch b/ui-ngx/patches/@angular+compiler+18.2.13.patch new file mode 100644 index 0000000000..a4245cb0e9 --- /dev/null +++ b/ui-ngx/patches/@angular+compiler+18.2.13.patch @@ -0,0 +1,88 @@ +diff --git a/node_modules/@angular/compiler/fesm2022/compiler.mjs b/node_modules/@angular/compiler/fesm2022/compiler.mjs +index a00b189..260e7be 100755 +--- a/node_modules/@angular/compiler/fesm2022/compiler.mjs ++++ b/node_modules/@angular/compiler/fesm2022/compiler.mjs +@@ -18631,6 +18631,7 @@ function SECURITY_SCHEMA() { + 'area|ping', + 'audio|src', + 'a|href', ++ 'a|xlink:href', + 'a|ping', + 'blockquote|cite', + 'body|background', +@@ -18644,6 +18645,75 @@ function SECURITY_SCHEMA() { + 'track|src', + 'video|poster', + 'video|src', ++ ++ // MathML namespace ++ // https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1 ++ 'annotation|href', ++ 'annotation|xlink:href', ++ 'annotation-xml|href', ++ 'annotation-xml|xlink:href', ++ 'maction|href', ++ 'maction|xlink:href', ++ 'malignmark|href', ++ 'malignmark|xlink:href', ++ 'math|href', ++ 'math|xlink:href', ++ 'mroot|href', ++ 'mroot|xlink:href', ++ 'msqrt|href', ++ 'msqrt|xlink:href', ++ 'merror|href', ++ 'merror|xlink:href', ++ 'mfrac|href', ++ 'mfrac|xlink:href', ++ 'mglyph|href', ++ 'mglyph|xlink:href', ++ 'msub|href', ++ 'msub|xlink:href', ++ 'msup|href', ++ 'msup|xlink:href', ++ 'msubsup|href', ++ 'msubsup|xlink:href', ++ 'mmultiscripts|href', ++ 'mmultiscripts|xlink:href', ++ 'mprescripts|href', ++ 'mprescripts|xlink:href', ++ 'mi|href', ++ 'mi|xlink:href', ++ 'mn|href', ++ 'mn|xlink:href', ++ 'mo|href', ++ 'mo|xlink:href', ++ 'mpadded|href', ++ 'mpadded|xlink:href', ++ 'mphantom|href', ++ 'mphantom|xlink:href', ++ 'mrow|href', ++ 'mrow|xlink:href', ++ 'ms|href', ++ 'ms|xlink:href', ++ 'mspace|href', ++ 'mspace|xlink:href', ++ 'mstyle|href', ++ 'mstyle|xlink:href', ++ 'mtable|href', ++ 'mtable|xlink:href', ++ 'mtd|href', ++ 'mtd|xlink:href', ++ 'mtr|href', ++ 'mtr|xlink:href', ++ 'mtext|href', ++ 'mtext|xlink:href', ++ 'mover|href', ++ 'mover|xlink:href', ++ 'munder|href', ++ 'munder|xlink:href', ++ 'munderover|href', ++ 'munderover|xlink:href', ++ 'semantics|href', ++ 'semantics|xlink:href', ++ 'none|href', ++ 'none|xlink:href', + ]); + registerContext(SecurityContext.RESOURCE_URL, [ + 'applet|code', diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 21759fbca0..9218b09df1 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -37,6 +37,7 @@ export interface SysParamsState { maxRelationLevelPerCfArgument: number; ruleChainDebugPerTenantLimitsConfiguration?: string; calculatedFieldDebugPerTenantLimitsConfiguration?: string; + intermediateAggregationIntervalInSecForCF: number; trendzSettings: TrendzSettings; } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index af040a6d53..aad609356a 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -39,6 +39,7 @@ const emptyUserAuthState: AuthPayload = { maxRelationLevelPerCfArgument: 0, maxDataPointsPerRollingArg: 0, maxDebugModeDurationMinutes: 0, + intermediateAggregationIntervalInSecForCF: 0, userSettings: initialUserSettings, trendzSettings: initialTrendzSettings }; 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 20bd3e1bde..343c3123ab 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 @@ -29,10 +29,11 @@
{{ 'common.general' | translate }}
-
+
{{ 'alarm-rule.alarm-type' | translate }} + alarm-rule.alarm-type-hint @if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) { @if (fieldFormGroup.get('name').hasError('required')) { @@ -48,15 +49,18 @@
@if (!data.entityId) { -
- + @if (fieldFormGroup.get('entityId.entityType').value) { - }
@@ -85,8 +90,8 @@ [tenantId]="data.tenantId" [ownerId]="data.ownerId" [watchKeyChange]="true" - [disabledAddButton]="!fieldFormGroup.get('entityId.id').value" - [entityName]="data.entityName"/> + [disable]="!fieldFormGroup.get('entityId.id').value || !fieldFormGroup.get('name').value" + [entityName]="entityName"/>
{{ 'alarm-rule.create-conditions' | translate }}
@@ -113,11 +118,11 @@
- alarm-rule.no-clear-alarm-rule + alarm-rule.no-clear-alarm-rule
@if (configFormGroup.get('propagate').value) { - - alarm-rule.alarm-rule-relation-types-list - - - {{key}} - close - - - - - + + }
@@ -181,7 +177,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index f45e205246..fea0df6024 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -40,6 +40,11 @@ import { import { deepTrim } from "@core/utils"; import { Observable } from "rxjs"; import { switchMap } from "rxjs/operators"; +import { EntityTypeSelectComponent } from "@shared/components/entity/entity-type-select.component"; +import { EntityAutocompleteComponent } from "@shared/components/entity/entity-autocomplete.component"; +import { EntityService } from "@core/http/entity.service"; +import { RelationTypes } from "@shared/models/relation.models"; +import { StringItemsOption } from "@shared/components/string-items-list.component"; export interface AlarmRuleDialogData { value?: CalculatedField; @@ -66,7 +71,7 @@ export class AlarmRuleDialogComponent extends DialogComponent(null, Validators.required), + entityType: this.fb.control(EntityType.DEVICE_PROFILE, Validators.required), id: [null as null | string, Validators.required], }), configuration: this.fb.group({ @@ -93,15 +98,45 @@ export class AlarmRuleDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, protected dialogRef: MatDialogRef, private calculatedFieldsService: CalculatedFieldsService, + private entityService: EntityService, private destroyRef: DestroyRef, private fb: FormBuilder) { super(store, router, dialogRef); this.applyDialogData(); + this.updateRulesValidators(); + + this.fieldFormGroup.get('configuration.arguments').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateRulesValidators(); + }); + + if (!this.entityName) { + this.fieldFormGroup.get('entityId.id').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((entityId) => { + if (entityId && (this.fieldFormGroup.get('entityId.entityType').value === EntityType.DEVICE_PROFILE || + this.fieldFormGroup.get('entityId.entityType').value === EntityType.ASSET_PROFILE)) { + this.entityService.getEntity(this.fieldFormGroup.get('entityId.entityType').value as EntityType, entityId, {ignoreLoading: true, ignoreErrors: true}).subscribe( + value => { + this.entityName = value.name; + } + ) + } + }); + } } get configFormGroup(): FormGroup { @@ -169,6 +204,10 @@ export class AlarmRuleDialogComponent extends DialogComponent this.dialogRef.close(calculatedField)); + } else { + this.fieldFormGroup.get('name').markAsTouched(); + this.entityTypeSelect.markAsTouched(); + this.entityAutocompleteComponent.markAsTouched(); } } @@ -191,4 +230,23 @@ export class AlarmRuleDialogComponent extends DialogComponent 0) { + this.fieldFormGroup.get('configuration.createRules').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration.clearRule').enable({emitEvent: false}); + this.disabledClearRuleButton = true; + } else { + this.fieldFormGroup.get('configuration.createRules').disable({emitEvent: false}); + this.fieldFormGroup.get('configuration.clearRule').disable({emitEvent: false}); + this.disabledClearRuleButton = false; + } + } + get predefinedTypeValues(): StringItemsOption[] { + return RelationTypes.map(type => ({ + name: type, + value: type + })); + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index b20f1cee7b..f73633ad8e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -287,7 +287,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private importCalculatedField(): void { - this.importExportService.openCalculatedFieldImportDialog() + this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file') .pipe( filter(Boolean), switchMap(calculatedField => { 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 d67cd51a9a..e7bc32a19f 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 @@ -75,14 +75,14 @@ matTooltipPosition="above" class="tb-mat-32" [disabled]="!argumentsList.length" - (click)="onTestScript()"> + (click)="onTestScript($event)"> bug_report
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 f565a66168..20ad4a4bfb 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 @@ -41,6 +41,7 @@ import { alarmRuleDefaultScript, AlarmRuleExpressionType, AlarmRuleFilter, + areFiltersAndPredicateArgumentsValid, filterOperationTranslationMap } from "@shared/models/alarm-rule.models"; @@ -137,7 +138,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { - this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList); + this.filtersValid = areFiltersAndPredicateArgumentsValid(filters, this.data.arguments); this.checkIsNoData(filters); }); @@ -206,39 +207,6 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { this.conditionFormGroup.get('expression.expression').setValue(expression); 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 638075849f..4892fcdecd 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 @@ -26,13 +26,15 @@
- + {{ conditionSet() ? 'edit' : 'add' }}
@@ -48,7 +50,10 @@ (click)="openScheduleDialog($event)">
- edit + + edit +
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 6f17aa8b59..08004d7497 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 @@ -32,7 +32,7 @@ import { getAlarmScheduleRangeText, utcTimestampToTimeOfDay } from '@shared/models/device.models'; -import { TimeUnit } from '@shared/models/time/time.models'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { CfAlarmRuleConditionDialogComponent, CfAlarmRuleConditionDialogData @@ -42,7 +42,8 @@ import { AlarmRuleConditionType, AlarmRuleExpressionType, AlarmRuleSchedule, - AlarmRuleScheduleType + AlarmRuleScheduleType, + checkPredicates } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { @@ -154,32 +155,17 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali return !arg || validArguments.includes(arg); } - private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean { - const validSet = new Set(validArguments); + private areFilterAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); 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)) { + if (!checkPredicates(filter.predicates, validSet)) { return false; } } @@ -192,7 +178,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali } public validate(control: AbstractControl): ValidationErrors | null { - this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments)); + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); this.onValidatorChange = () => { control.updateValueAndValidity({ emitEvent: true }); @@ -265,7 +251,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (this.modelValue.value.dynamicValueArgument) { this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { attribute: `${this.modelValue.value.dynamicValueArgument}` - }); + }) + ' ' + this.translate.instant(timeUnitTranslationMap.get(this.modelValue.unit)).toLowerCase(); } else { this.specText = this.translate.instant('alarm-rule.condition-during', { during: duringText 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 a9f4ad61a5..a7ccc25fe6 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 @@ -18,33 +18,29 @@
- @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { -
-
- alarm-rule.alarm-rule-additional-info -
- - - - +
+
+ alarm-rule.alarm-rule-additional-info
- } - @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { -
-
- alarm-rule.alarm-rule-mobile-dashboard -
- - + + + + +
+
+
+ alarm-rule.alarm-rule-mobile-dashboard
- } + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html index 359ad104f1..bde332b383 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -16,7 +16,7 @@ --> -
+
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
} -
- + @if (!createAlarmRulesFormArray().controls.length) { + alarm-rule.add-create-alarm-rule-prompt - -
-
+ + } +
+
- } -
-
-
-
{{ filterControl.value?.argument }}
-
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
- -
+ @if (index) { + + }
- @if (index) { - - } -
- } - @if (!filtersFormArray.length) { - - alarm-rule.no-filter - - - } -
- + } +
+ +} @else { + alarm-rule.no-filter +} + +
+ @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { +
+ @if (index) { +
+ {{ complexOperationTranslations.get(operation) | translate }} +
+ } +
+
+ + + +
-
- } - @if (!predicatesFormArray.length) { - - alarm-rule.no-filter - - - } + } +
-
-
+ } @else { + alarm-rule.no-filter + } +
} } - @if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) { - - - } @else if (type !== filterPredicateType.COMPLEX) { - - - } + + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts index 2f542f048e..32791ade96 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts @@ -34,6 +34,7 @@ import { alarmRuleNumericOperationTranslationMap, AlarmRuleStringOperation, alarmRuleStringOperationTranslationMap, + checkPredicates, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; import { MatDialog } from "@angular/material/dialog"; @@ -111,6 +112,18 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, this.updateModel(); }); + this.filterPredicateFormGroup.get('operation').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => { + if (value === 'NO_DATA') { + this.filterPredicateFormGroup.get('duration').enable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.get('duration').disable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').enable({emitEvent: false}); + } + }) + this.filterPredicateFormGroup.get('predicates').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(predicates => { @@ -140,25 +153,10 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, } } - private isPredicateArgumentsValid(predicates: any): boolean { + private isPredicateArgumentsValid(predicates: AlarmRuleFilterPredicate[]): 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)) { + if (!checkPredicates(predicates, validSet)) { return false; } } @@ -172,8 +170,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, } if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) { this.type = AlarmRuleFilterPredicateType[this.valueType]; + this.filterPredicateFormGroup.get('duration').enable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').disable({emitEvent: false}); this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false}); } else { + this.filterPredicateFormGroup.get('duration').disable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').enable({emitEvent: false}); this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); } } @@ -183,30 +185,6 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, 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}); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html index 4ecba0f3d2..d070c2de84 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html @@ -18,6 +18,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss index 8712fdfd32..dbaf7dd966 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss @@ -23,6 +23,9 @@ color: #f44336; padding: 0; } + &.disabled { + color: rgba(0,0,0,0.38); + } &.nowrap { white-space: nowrap; text-overflow: ellipsis; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts index 703c41665e..aa8aa2bfec 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts @@ -63,6 +63,10 @@ export class AlarmRuleFilterTextComponent { @Input() arguments: Record; + @Input() + @coerceBoolean() + disabled = false; + private alarmRuleExpressionValue: AlarmRuleExpression; get alarmRuleExpression(): AlarmRuleExpression { return this.alarmRuleExpressionValue; diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index cd87bde8f2..0dc4e0a90f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -30,16 +30,18 @@
- - attribute.key - - - {{ (isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required') | translate }} - - - {{ 'attribute.key-max-length' | translate }} - - + + diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts index 1336236442..3dd1a09a8c 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts @@ -23,13 +23,23 @@ import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Valida import { EntityId } from '@shared/models/id/entity-id'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; -import { AttributeData, AttributeScope, LatestTelemetry, TelemetryType } from '@shared/models/telemetry/telemetry.models'; +import { + AttributeData, + AttributeScope, + LatestTelemetry, + TelemetryType +} from '@shared/models/telemetry/telemetry.models'; import { AttributeService } from '@core/http/attribute.service'; import { Observable } from 'rxjs'; +import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; +import { map } from 'rxjs/operators'; +import { ErrorMessageConfig } from '@shared/components/string-autocomplete.component'; +import { TranslateService } from '@ngx-translate/core'; export interface AddAttributeDialogData { entityId: EntityId; attributeScope: TelemetryType; + datasource?: AttributeDatasource; } @Component({ @@ -47,19 +57,22 @@ export class AddAttributeDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData, private attributeService: AttributeService, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - public fb: FormBuilder) { + public fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); } ngOnInit(): void { this.attributeFormGroup = this.fb.group({ - key: ['', [Validators.required, Validators.maxLength(255)]], + key: ['', this.keyValidators], value: [null, [Validators.required]] }); this.isTelemetry = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY; @@ -97,4 +110,18 @@ export class AddAttributeDialogComponent extends DialogComponent this.dialogRef.close(true)); } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return this.data.datasource?.getAllAttributes(this.data.entityId,this.data.attributeScope).pipe( + map(attributes => attributes?.filter(attribute => attribute.key.toLowerCase().includes(search)).map(a => a.key)), + ) + } + + get attributeErrorMessages(): ErrorMessageConfig { + return { + required: this.translate.instant(this.isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required'), + maxlength: this.translate.instant('attribute.key-max-length') + } + } } diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index bc9059d80a..c81c33f741 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -317,13 +317,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if ($event) { $event.stopPropagation(); } + const data: AddAttributeDialogData = { + entityId: this.entityIdValue, + attributeScope: this.attributeScope, + }; + + if(this.attributeScope === LatestTelemetry.LATEST_TELEMETRY) { + data.datasource = this.dataSource; + } + this.dialog.open(AddAttributeDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - entityId: this.entityIdValue, - attributeScope: this.attributeScope - } + data }).afterClosed().subscribe( (res) => { if (res) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index 6b8f4fda79..d887c0f40a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -15,8 +15,10 @@ limitations under the License. --> -
-
+
+
@@ -94,6 +96,7 @@
-
- {{ 'calculated-fields.no-arguments' | translate }} -
@if (errorText || (dataSource.isEmpty() | async)) { }
+
+ {{ 'calculated-fields.no-arguments' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index bca335211e..002a515164 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, DestroyRef, @@ -87,7 +88,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces @Input() entityName: string; @Input() ownerId: EntityId; @Input() isScript: boolean; - @Input() disabledAddButton = false; + @Input({transform: booleanAttribute}) disable = false; @Input() watchKeyChange = false; @ViewChild(MatSort, { static: true }) sort: MatSort; @@ -220,8 +221,6 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; } else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; - } else if (!this.argumentsFormArray.controls.length) { - this.errorText = 'calculated-fields.hint.arguments-empty'; } else { this.errorText = ''; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 3070a6cbb2..14ec6d1ce8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ 'entity.type-calculated-field' | translate}}

diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html index 4dcbf5b38d..c49c6f25cc 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -124,6 +124,15 @@ }
+
+ +
+ {{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }} +
+
+
Observable; readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; + readonly intermediateAggregationIntervalInSecForCF = getCurrentAuthState(this.store).intermediateAggregationIntervalInSecForCF; readonly DayInSec = DAY / SECOND; entityAggregationConfiguration = this.fb.group({ @@ -104,6 +105,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor watermark: this.fb.group({ duration: [HOUR/SECOND, Validators.required], }), + produceIntermediateResult: [false], output: this.fb.control(defaultCalculatedFieldOutput), }); @@ -153,6 +155,15 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.updatedOffsetHint(); }); + merge( + this.entityAggregationConfiguration.get('interval.type').valueChanges, + this.entityAggregationConfiguration.get('interval.durationSec').valueChanges + ).pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.checkProduceIntermediate(); + }); + this.entityAggregationConfiguration.valueChanges.pipe( takeUntilDestroyed() ).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { @@ -174,6 +185,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); this.updatedOffsetHint(); setTimeout(() => { this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); @@ -194,6 +206,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); } } @@ -255,16 +268,49 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor } } + private checkProduceIntermediate() { + const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; + let durationSec = 0; + switch (intervalType) { + case AggIntervalType.CUSTOM: + durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; + break + case AggIntervalType.HOUR: + durationSec = HOUR / SECOND; + break + case AggIntervalType.DAY: + durationSec = DAY / SECOND; + break + case AggIntervalType.WEEK: + case AggIntervalType.WEEK_SUN_SAT: + durationSec = WEEK / SECOND; + break + case AggIntervalType.MONTH: + durationSec = AVG_MONTH / SECOND; + break + case AggIntervalType.QUARTER: + durationSec = AVG_QUARTER / SECOND; + break + case AggIntervalType.YEAR: + durationSec = YEAR / SECOND; + break + } + if (durationSec > this.intermediateAggregationIntervalInSecForCF) { + this.entityAggregationConfiguration.get('produceIntermediateResult').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('produceIntermediateResult').disable({emitEvent: false}); + } + } + private updatedOffsetHint(): void { const offset = this.entityAggregationConfiguration.get('interval.offsetSec').value; const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; const durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; const offsetCategory = this.getTimeCategory(offset); const now = _moment.utc(); - let interval: string = ''; + let interval: string; if (intervalType === AggIntervalType.CUSTOM) { - const durationSecCategory = this.getTimeCategory(durationSec); - const formatString = this.getCustomFormatString(offsetCategory, durationSecCategory); + const formatString = this.getCustomFormatString(offsetCategory, this.getTimeCategory(durationSec)); const intervals: string[] = []; let allInterval = durationSec >= HOUR*6/SECOND && durationSec < DAY/SECOND; now.startOf('year').add(offset, 'seconds'); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index dcfd37796d..67ec7f097b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -16,7 +16,9 @@ -->
-
+
@@ -121,14 +123,14 @@ *matHeaderRowDef="['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']">
-
- {{ 'calculated-fields.no-zone-configured' | translate }} -
@if (errorText) { }
+
+ {{ 'calculated-fields.no-zone-configured' | translate }} +
-
- -
-
calculated-fields.use-latest-timestamp
-
-
-
+ @if (relatedAggregationConfiguration.get('output').value?.type === OutputType.Timeseries) { +
+ +
+
calculated-fields.use-latest-timestamp
+
+
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html index cfab9d9def..178f8b47d6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -23,6 +23,7 @@ [tenantId]="tenantId" [ownerId]="ownerId" [entityName]="entityName" + [watchKeyChange]="true" [isScript]="isScript" />
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 4c311333ec..8a93423e4d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -368,6 +368,34 @@
+
+ + tenant-profile.intermediate-aggregation-interval + + + {{ 'tenant-profile.intermediate-aggregation-interval-required' | translate}} + + + {{ 'tenant-profile.intermediate-aggregation-interval-range' | translate}} + + + + + tenant-profile.reevaluation-check-interval + + + {{ 'tenant-profile.reevaluation-check-interval-required' | translate}} + + + {{ 'tenant-profile.reevaluation-check-interval-range' | translate}} + + + +
tenant-profile.relation-search-entity-limit @@ -526,6 +554,22 @@
+
+ + tenant-profile.alarms-reevaluation-interval + + + {{ 'tenant-profile.alarms-reevaluation-interval-required' | translate}} + + + {{ 'tenant-profile.alarms-reevaluation-interval-range' | translate}} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index a61a1aa1f8..109ec3d3d8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -120,6 +120,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA minAllowedAggregationIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], maxRelatedEntitiesToReturnPerCfArgument: [1, [Validators.required, Validators.min(1)]], minAllowedScheduledUpdateIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + intermediateAggregationIntervalInSecForCF: [0, [Validators.required, Validators.min(1)]], + cfReevaluationCheckInterval: [0, [Validators.required, Validators.min(1)]], + alarmsReevaluationInterval: [0, [Validators.required, Validators.min(1)]], maxDataPointsPerRollingArg: [0, [Validators.required, Validators.min(0)]], maxStateSizeInKBytes: [0, [Validators.required, Validators.min(0)]], calculatedFieldDebugEventsRateLimit: [''], diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index e836e5e28b..67f4f86970 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -104,8 +104,20 @@ {{ 'relation.type' | translate }} - - {{ relation.type }} + +
+ {{ relation.type }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss index 263ebfeed3..a7dffdfe0a 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -59,6 +59,17 @@ overflow: hidden; text-overflow: ellipsis; } + + .type-copy { + visibility: hidden; + transition: visibility 0.1s; + } + + .type:hover { + .type-copy { + visibility: visible; + } + } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 94bd0b2cf0..24f9ec1bd7 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -15,8 +15,11 @@ limitations under the License. --> -