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 97d5d0dcea..f1bd3bd58d 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(); } @@ -877,4 +884,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..c7c630c3b3 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 @@ -87,16 +87,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) { @@ -116,6 +115,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; 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 489632b9b9..f6c577545e 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 @@ -60,6 +60,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; @@ -121,6 +122,7 @@ public class CalculatedFieldCtx implements Closeable { private long maxStateSize; private long maxSingleValueArgumentSize; + private long intermediateAggregationIntervalMillis; private boolean relationQueryDynamicArguments; private List mainEntityGeofencingArgumentNames; @@ -128,6 +130,8 @@ public class CalculatedFieldCtx implements Closeable { private List relatedEntityArgumentNames; private long scheduledUpdateIntervalMillis; + private long cfCheckReevaluationInterval; + private long alarmReevaluationInterval; private Argument propagationArgument; private boolean applyExpressionForResolvedArguments; @@ -214,8 +218,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() { @@ -229,6 +232,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) { @@ -239,7 +248,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; @@ -294,9 +303,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) { @@ -692,7 +705,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/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/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/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/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/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/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. --> -