Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into cf-output

pull/14535/head
IrynaMatveieva 10 months ago
parent
commit
c31469fce2
  1. 12
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 8
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 26
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  4. 1
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  5. 9
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  7. 13
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  8. 28
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  10. 14
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  11. 14
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  13. 5
      application/src/main/resources/thingsboard.yml
  14. 62
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  15. 36
      application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java
  16. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  17. 1
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java
  18. 25
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  19. 2
      common/queue/pom.xml
  20. 4
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
  21. 2
      msa/js-executor/package.json
  22. 14
      msa/js-executor/yarn.lock
  23. 2
      msa/web-ui/package.json
  24. 14
      msa/web-ui/yarn.lock
  25. 14
      pom.xml
  26. 2
      rule-engine/rule-engine-components/pom.xml
  27. 4
      tools/pom.xml
  28. 66
      ui-ngx/patches/@angular+common+18.2.13.patch
  29. 88
      ui-ngx/patches/@angular+compiler+18.2.13.patch
  30. 1
      ui-ngx/src/app/core/auth/auth.models.ts
  31. 1
      ui-ngx/src/app/core/auth/auth.reducer.ts
  32. 60
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  33. 62
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  34. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  35. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  36. 45
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  37. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  38. 30
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  39. 50
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  40. 14
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  41. 33
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  42. 115
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  43. 42
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  44. 92
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  45. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts
  46. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  47. 27
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  48. 60
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  49. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
  50. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
  51. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
  52. 22
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  53. 33
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts
  54. 14
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  55. 18
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  56. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  57. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  58. 9
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  59. 54
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  60. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
  61. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  62. 15
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html
  63. 16
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  64. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html
  65. 44
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  66. 3
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  67. 16
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  68. 11
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss
  69. 11
      ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html
  70. 6
      ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss
  71. 8
      ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts
  72. 4
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  73. 4
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
  74. 8
      ui-ngx/src/app/shared/components/string-autocomplete.component.html
  75. 20
      ui-ngx/src/app/shared/components/string-autocomplete.component.scss
  76. 50
      ui-ngx/src/app/shared/components/string-autocomplete.component.ts
  77. 4
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  78. 70
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  79. 1
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  80. 9
      ui-ngx/src/app/shared/models/tenant.model.ts
  81. 2
      ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md
  82. 43
      ui-ngx/src/assets/locale/locale.constant-en_US.json

12
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'
]
);

8
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;

26
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;
}
}
}

1
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))

9
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" +

2
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

13
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;

28
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<String> mainEntityGeofencingArgumentNames;
@ -128,6 +130,8 @@ public class CalculatedFieldCtx implements Closeable {
private List<String> 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;
}

4
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();

14
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<String, AggMetric> 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<String, AggIntervalEntryStatus> args,
Map<AggIntervalEntry, Map<String, ArgumentEntry>> 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);

14
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);

2
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;

5
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:

62
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<String, Argument> arguments = Map.of(
"temperature", temperatureArgument
);
Map<AlarmSeverity, Condition> 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<String, Argument> arguments = Map.of(
"powerConsumption", powerConsumptionArgument
);
long createDurationMs = 2000L;
Map<AlarmSeverity, Condition> createRules = Map.of(
AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, null,
new AlarmConditionValue<Long>(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();

36
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)

1
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;
}

1
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;

25
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;
}
}

2
common/queue/pom.xml

@ -69,7 +69,7 @@
<artifactId>kafka-clients</artifactId>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency>

4
dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java

@ -50,7 +50,7 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
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<CalculatedField>
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;
}

2
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",

14
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"

2
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"
},

14
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"

14
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 -->
<kafka.version>3.9.1</kafka.version>
<lz4.version>1.8.1</lz4.version> <!-- to fix CVE-2025-12183 introduced through kafka-clients 3.9.1 TODO: remove when kafka-clients is bumped -->
<lz4.version>1.10.1</lz4.version> <!-- to fix CVE-2025-12183 and CVE-2025-66566 introduced through kafka-clients 3.9.1 TODO: remove when kafka-clients is bumped -->
<bucket4j.version>8.10.1</bucket4j.version>
<antlr.version>3.5.3</antlr.version>
<snakeyaml.version>2.2</snakeyaml.version>
@ -102,7 +102,7 @@
<passay.version>1.6.4</passay.version>
<ua-parser.version>1.6.1</ua-parser.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
<commons-beanutils.version>1.11.0</commons-beanutils.version>
<commons-collections.version>4.4</commons-collections.version>
<protobuf-dynamic.version>1.0.4TB</protobuf-dynamic.version>
<wire-schema.version>3.7.1</wire-schema.version>
@ -147,7 +147,7 @@
<firebase-admin.version>9.2.0</firebase-admin.version>
<snappy.version>1.1.10.5</snappy.version>
<rocksdbjni.version>9.10.0</rocksdbjni.version>
<netty.version>4.1.125.Final</netty.version> <!-- to fix CVEs. TODO: remove when fixed in spring-boot-dependencies -->
<netty.version>4.1.128.Final</netty.version> <!-- to fix CVEs. TODO: remove when fixed in spring-boot-dependencies -->
</properties>
<modules>
@ -1162,7 +1162,7 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
<version>${lz4.version}</version> <!-- to fix CVE introduced through kafka-clients 3.9.1 -->
</dependency>
@ -1445,6 +1445,12 @@
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
<version>${cassandra-all.version}</version>
<exclusions>
<exclusion>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testng</groupId>

2
rule-engine/rule-engine-components/pom.xml

@ -97,7 +97,7 @@
<artifactId>kafka-clients</artifactId>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency>

4
tools/pom.xml

@ -55,6 +55,10 @@
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
</dependency>
<dependency>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>

66
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();

88
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',

1
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;
}

1
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
};

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

@ -29,10 +29,11 @@
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2">
<div class="flex gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'alarm-rule.alarm-type' | translate }}</mat-label>
<input matInput maxlength="255" formControlName="name" required>
<mat-hint translate>alarm-rule.alarm-type-hint</mat-hint>
@if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) {
<mat-error>
@if (fieldFormGroup.get('name').hasError('required')) {
@ -48,15 +49,18 @@
<tb-entity-debug-settings-button
formControlName="debugSettings"
entityLabel="alarm-rule.alarm-rule"
class="pt-[10px]"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
@if (!data.entityId) {
<div class="flex flex-col gap-4" formGroupName="entityId">
<tb-entity-type-select appearance="outline"
<div class="flex flex-row xs:flex-col gap-4" formGroupName="entityId">
<tb-entity-type-select #entityTypeSelect
appearance="outline"
subscriptSizing="dynamic"
class="flex-1"
showLabel
required
label="{{ 'alarm-rule.target-entity-type' | translate }}"
@ -65,13 +69,14 @@
formControlName="entityType">
</tb-entity-type-select>
@if (fieldFormGroup.get('entityId.entityType').value) {
<tb-entity-autocomplete
formControlName="id"
appearance="outline"
subscriptSizing="dynamic"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
<tb-entity-autocomplete #entityAutocompleteComponent
formControlName="id"
appearance="outline"
subscriptSizing="dynamic"
class="flex-1"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
/>
}
</div>
@ -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"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div>
@ -113,11 +118,11 @@
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate style="margin: 16px 0"
class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
<span translate class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
</div>
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
[disabled]="!disabledClearRuleButton"
type="button"
(click)="addClearAlarmRule()">
{{ 'alarm-rule.add-clear-alarm-rule' | translate }}
@ -137,23 +142,14 @@
</mat-slide-toggle>
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block p-4" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
<tb-string-items-list appearance="outline"
class="flex flex-1 px-4"
label="{{ 'alarm-rule.alarm-rule-relation-types-list' | translate }}"
placeholder="{{ 'alarm-rule.alarm-rule-relation-types-list' | translate }}"
hint="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"
[predefinedValues]="predefinedTypeValues"
formControlName="propagateRelationTypes">
</tb-string-items-list>
}
</div>
<div class="tb-form-row">
@ -181,7 +177,7 @@
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty">
[disabled]="isLoading$ | async">
{{ data.buttonTitle | translate }}
</button>
</div>

62
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<AlarmRuleDialogCom
type: [CalculatedFieldType.ALARM],
debugSettings: [],
entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(null, Validators.required),
entityType: this.fb.control<EntityType | AliasEntityType | null>(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<AlarmRuleDialogCom
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
entityName = this.data.entityName;
disabledClearRuleButton = false;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
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<AlarmRuleDialogCom
this.calculatedFieldsService.saveCalculatedField(alarmRule)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(calculatedField => 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<AlarmRuleDialogCom
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, null, false);
}
private updateRulesValidators(): void {
if (Object.keys(this.arguments ?? {}).length > 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
}));
}
}

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

@ -287,7 +287,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
}
private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog()
this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file')
.pipe(
filter(Boolean),
switchMap(calculatedField => {

4
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)">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
(click)="onTestScript($event)"
[disabled]="!argumentsList.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>

45
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<CfAlarm
type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE,
unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: {
staticValue: this.condition?.value?.staticValue,
staticValue: this.condition?.value?.staticValue ?? null,
dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.value?.dynamicValueArgument) ? this.condition?.value?.dynamicValueArgument : null,
},
count: {
@ -150,7 +151,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false});
this.filtersValid = this.areFilterAndPredicateArgumentsValid(this.condition?.expression?.filters, this.argumentsList);
this.filtersValid = areFiltersAndPredicateArgumentsValid(this.condition?.expression?.filters, this.data.arguments);
this.checkIsNoData(this.condition?.expression?.filters);
this.conditionFormGroup.get('type').valueChanges.pipe(
@ -168,7 +169,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.conditionFormGroup.get('expression.filters').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((filters) => {
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<CfAlarm
}
}
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
const filters = obj || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
}
return true;
}
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
@ -324,7 +292,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
if (value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${value.dynamicValueArgument}`
});
}) + ' ' + this.translate.instant(this.timeUnitTranslations.get(this.conditionFormGroup.get('unit').value)).toLowerCase();
} else {
this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText
@ -355,7 +323,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
}
onTestScript() {
onTestScript($event: Event) {
$event?.preventDefault();
this.data.testScript(this.conditionFormGroup.get('expression.expression').value).subscribe(
(expression) => {
this.conditionFormGroup.get('expression.expression').setValue(expression);

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

@ -26,13 +26,15 @@
<div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
[arguments]="arguments"
[disabled]="disabled"
class="flex-1"
[nowrap]="true"
[specText]="specText"
required
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
<mat-icon [color]="disabled ? '' : (conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn')"
class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
{{ conditionSet() ? 'edit' : 'add' }}
</mat-icon>
</div>
@ -48,7 +50,10 @@
(click)="openScheduleDialog($event)">
<div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span>
<mat-icon [color]="schedulerArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
<mat-icon [color]="disabled ? '' : (schedulerArgumentsValid ? 'primary' : 'warn')"
class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
edit
</mat-icon>
</div>
</button>
</div>

30
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<string, CalculatedFieldArgument>): 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

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

@ -18,33 +18,29 @@
<div class="tb-form-panel no-border no-padding" [formGroup]="alarmRuleFormGroup">
<tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required" [testScript]="testScript" [isClearCondition]="isClearCondition">
</tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-additional-info-icon-hint' | translate }}" translate>
alarm-rule.alarm-rule-additional-info
</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<button type="button"
matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)">
<mat-icon class="material-icons">open_in_new</mat-icon>
</button>
</mat-form-field>
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-additional-info-icon-hint' | translate }}" translate>
alarm-rule.alarm-rule-additional-info
</div>
}
@if (!disabled || alarmRuleFormGroup.get('dashboardId').value) {
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-mobile-dashboard-hint' | translate}}" translate>
alarm-rule.alarm-rule-mobile-dashboard
</div>
<tb-dashboard-autocomplete appearance="outline"
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<button type="button"
matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)">
<mat-icon class="material-icons">open_in_new</mat-icon>
</button>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-mobile-dashboard-hint' | translate}}" translate>
alarm-rule.alarm-rule-mobile-dashboard
</div>
}
<tb-dashboard-autocomplete appearance="outline"
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
</div>
</div>

14
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html

@ -16,7 +16,7 @@
-->
<div class="flex flex-1 flex-col">
<div class="flex flex-1 flex-col gap-4">
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
<div class="flex flex-row items-center justify-start gap-2 pb-2" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule flex flex-1 flex-col gap-4"
@ -48,14 +48,14 @@
</button>
</div>
}
<div *ngIf="!createAlarmRulesFormArray().controls.length && !disabled">
<span translate class="flex items-center justify-center tb-prompt required" style="margin: 16px 0">
@if (!createAlarmRulesFormArray().controls.length) {
<span translate class="flex items-center justify-center tb-prompt required" [class.required]="!disabled">
alarm-rule.add-create-alarm-rule-prompt
</span>
</div>
<div *ngIf="!disabled">
</span>
}
<div>
<button mat-stroked-button color="primary"
[disabled]="createAlarmRulesFormArray().controls.length > 4"
[disabled]="createAlarmRulesFormArray().controls.length > 4 || disabled"
type="button"
(click)="addCreateAlarmRule()">
{{ 'alarm-rule.add-create-alarm-rule' | translate }}

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

@ -27,12 +27,12 @@
import {
AlarmRuleFilter,
AlarmRuleFilterPredicate,
filterOperationTranslationMap
filterOperationTranslationMap,
isPredicateArgumentsValid
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { isDefinedAndNotNull } from "@core/utils";
export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter;
@ -87,11 +87,11 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
}
);
this.predicatesValid = this.isPredicateArgumentsValid(this.data.filter.predicates);
this.predicatesValid = isPredicateArgumentsValid(this.data.filter.predicates, this.arguments);
this.filterFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicatesValid = this.isPredicateArgumentsValid(predicates);
this.predicatesValid = isPredicateArgumentsValid(predicates, this.arguments);
});
this.filterFormGroup.get('valueType').valueChanges.pipe(
@ -116,31 +116,6 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
});
}
private isPredicateArgumentsValid(predicates: any): boolean {
const validSet = new Set(Object.keys(this.data.arguments));
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (isDefinedAndNotNull(p.value?.dynamicValueArgument)) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) {
return false;
}
}
return true;
}
argumentInUse(argument: string): boolean {
return this.data.usedArguments.includes(argument);
}

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

@ -15,69 +15,68 @@
limitations under the License.
-->
<section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup">
<div class="flex flex-row">
<span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span class="min-w-24">&nbsp;</span>
@if (filtersFormArray.length) {
<section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup">
<div class="flex flex-row">
<span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span class="min-w-24">&nbsp;</span>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="filter-list">
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start"
formArrayName="filters"
[class.filter-list-divider]="index">
<div class="filters-operation max-w-8% flex-full">
@if ($index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
<mat-divider></mat-divider>
<div class="filter-list">
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start"
formArrayName="filters"
[class.filter-list-divider]="index">
<div class="filters-operation max-w-8% flex-full">
@if ($index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
</div>
}
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div>
<div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above">
<mat-icon
[matBadgeHidden]="areFilterAndPredicateArgumentsValid(filterControl.value, arguments)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
}
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div>
<div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above">
<mat-icon
[matBadgeHidden]="areFilterAndPredicateArgumentsValid(filterControl.value)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
@if (index) {
<mat-divider></mat-divider>
}
</div>
@if (index) {
<mat-divider></mat-divider>
}
</div>
}
@if (!filtersFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div>
</section>
}
</div>
</section>
} @else {
<span class="no-data-found tb-prompt flex items-center justify-center" [class.required]="!disabled" translate>alarm-rule.no-filter</span>
}
<button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addFilter()"
type="button">
{{ 'alarm-rule.add-filter' | translate }}

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

@ -38,7 +38,11 @@ import {
AlarmRuleFilterDialogComponent,
AlarmRuleFilterDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component";
import { AlarmRuleFilter, FilterPredicateTypeTranslationMap } from "@shared/models/alarm-rule.models";
import {
AlarmRuleFilter,
areFilterAndPredicateArgumentsValid,
FilterPredicateTypeTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@ -71,6 +75,10 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
filters: this.fb.array([])
});
disabled = false;
areFilterAndPredicateArgumentsValid = areFilterAndPredicateArgumentsValid;
complexOperationTranslationMap = complexOperationTranslationMap;
FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap
@ -102,37 +110,8 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
};
}
public areFilterAndPredicateArgumentsValid(obj: any): boolean {
const validSet = new Set(Object.keys(this.arguments));
const filter = obj || [];
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
return true;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
@ -206,5 +185,4 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
const filters = this.filterListFormGroup.value.filters as Array<AlarmRuleFilter>;
this.propagateChange(filters);
}
}

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

@ -16,66 +16,62 @@
-->
<section class="flex flex-col gap-4" [formGroup]="filterListFormGroup">
<div class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row">
<div class="flex flex-full flex-row items-center justify-start gap-2">
<div class="flex flex-1 flex-row gap-4 pl-2">
<label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label>
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</label>
<label translate class="filter-title max-w-50% flex-full">alarm-rule.value</label>
@if (predicatesFormArray.length) {
<div class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row">
<div class="flex flex-full flex-row items-center justify-start gap-2">
<div class="flex flex-1 flex-row gap-4 pl-2">
<label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label>
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</label>
<label translate class="filter-title max-w-50% flex-full">alarm-rule.value</label>
</div>
<span [class.!hidden]="disabled" class="min-w-10">&nbsp;</span>
</div>
<span [class.!hidden]="disabled" class="min-w-10">&nbsp;</span>
</div>
</div>
<mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list">
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start relative"
[class.key-filter-list-divider]="$index">
@if (index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
}
<div class="flex flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start">
<tb-alarm-rule-filter-predicate
class="flex-1"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-alarm-rule-filter-predicate>
<button mat-icon-button color="primary"
[disabled]="disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
<mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list">
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start relative"
[class.key-filter-list-divider]="$index">
@if (index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
}
<div class="flex flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start">
<tb-alarm-rule-filter-predicate
class="flex-1"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-alarm-rule-filter-predicate>
<button mat-icon-button color="primary"
[disabled]="disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
</div>
}
@if (!predicatesFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
}
</div>
</div>
</div>
<div class="flex flex-row gap-2">
} @else {
<span class="no-data-found tb-prompt flex items-center justify-center" [class.required]="!disabled" translate>alarm-rule.no-filter</span>
}
<div class="flex flex-row gap-2" [class.!hidden]="disabled">
<button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button">
{{ 'action.add' | translate }}
</button>
<button mat-button
color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button">
{{ 'filter.add-complex' | translate }}

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

@ -150,7 +150,7 @@ export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValu
}
writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void {
if (isDefinedAndNotNull(predicateValue.duration.dynamicValueArgument)) {
if (isDefinedAndNotNull(predicateValue?.duration?.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) {
predicateValue.duration.dynamicValueArgument = '';

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

@ -166,7 +166,7 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
}
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
if (isDefinedAndNotNull(predicateValue.dynamicValueArgument)) {
if (isDefinedAndNotNull(predicateValue?.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.dynamicValueArgument)) {
predicateValue.dynamicValueArgument = '';

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

@ -70,20 +70,17 @@
</div>
}
}
@if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) {
<tb-alarm-rule-filter-predicate-no-data-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="duration">
</tb-alarm-rule-filter-predicate-no-data-value>
} @else if (type !== filterPredicateType.COMPLEX) {
<tb-alarm-rule-filter-predicate-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="value">
</tb-alarm-rule-filter-predicate-value>
}
<tb-alarm-rule-filter-predicate-no-data-value class="flex-full" [class.!hidden]="!(filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA)"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="duration">
</tb-alarm-rule-filter-predicate-no-data-value>
<tb-alarm-rule-filter-predicate-value class="flex-full" [class.!hidden]="!(filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA && type !== filterPredicateType.COMPLEX)"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="value">
</tb-alarm-rule-filter-predicate-value>
</div>
</div>

60
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});
}
}

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

@ -18,6 +18,7 @@
<div class="tb-filter-text"
tbTruncateWithTooltip
[class.required]="isRequired"
[class.disabled]="disabled"
[class.nowrap]="nowrap"
[class.wrap]="!nowrap"
[innerHTML]="filterText">

3
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;

4
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<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
disabled = false;
private alarmRuleExpressionValue: AlarmRuleExpression;
get alarmRuleExpression(): AlarmRuleExpression {
return this.alarmRuleExpressionValue;

22
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -30,16 +30,18 @@
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col gap-2">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="mat-block tb-inline-field">
<mat-label translate>attribute.key</mat-label>
<input matInput formControlName="key" required>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('required')">
{{ (isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required') | translate }}
</mat-error>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('maxlength')">
{{ 'attribute.key-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-string-autocomplete
panelWidth=""
additionalClass="mat-block tb-inline-field tb-suffix-absolute tb-suffix-show-on-hover"
required
[label]="'attribute.key' | translate"
subscriptSizing="dynamic"
appearance="outline"
[fetchOptionsFn]="isTelemetry && fetchOptions.bind(this)"
[errorMessages]="attributeErrorMessages"
[controlValidators]="keyValidators"
formControlName="key">
</tb-string-autocomplete>
<tb-value-input
formControlName="value"
[requiredText]="isTelemetry ? 'attribute.telemetry-value-required' : 'attribute.value-required'">

33
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<AddAttributeDia
isTelemetry = false;
keyValidators = [Validators.required, Validators.maxLength(255)];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData,
private attributeService: AttributeService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddAttributeDialogComponent, boolean>,
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<AddAttributeDia
}
task.subscribe(() => this.dialogRef.close(true));
}
fetchOptions(searchText: string): Observable<Array<string>> {
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')
}
}
}

14
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, AddAttributeDialogData, boolean>(AddAttributeDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entityId: this.entityIdValue,
attributeScope: this.attributeScope
}
data
}).afterClosed().subscribe(
(res) => {
if (res) {

18
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.
-->
<div class="flex flex-col gap-3">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col" [class.arguments-table-with-error]="errorText">
<div class="flex flex-col gap-4">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col"
[class.arguments-table-with-error]="errorText"
[class.!hidden]="!(dataSource.isEmpty() | async) === false">
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
@ -94,6 +96,7 @@
<button type="button"
mat-icon-button
#button
[disabled]="disable"
(click)="manageArgument($event, button, argument)"
[matTooltip]="'action.edit' | translate"
matTooltipPosition="above">
@ -119,14 +122,15 @@
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayColumns"></mat-header-row>
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center min-h-14">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
@if (errorText || (dataSource.isEmpty() | async)) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
[class.required]="!disable"
class="tb-prompt flex flex-1 items-end justify-center tb-prompt required">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
<div class="flex h-9 justify-between">
<button
type="button"
@ -134,7 +138,7 @@
color="primary"
#button
(click)="manageArgument($event, button)"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disabledAddButton"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disable"
>
{{ 'calculated-fields.add-argument' | translate }}
</button>

5
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 = '';
}

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex size-full max-w-4xl flex-col">
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex max-w-4xl flex-col">
<mat-toolbar color="primary">
<h2>{{ 'entity.type-calculated-field' | translate}}</h2>
<span class="flex-1"></span>

9
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html

@ -124,6 +124,15 @@
</ng-container>
}
</div>
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide flex" formControlName="produceIntermediateResult">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.produce-intermediate-result-hint' | translate: {
time: (intermediateAggregationIntervalInSecForCF * 1000 | milliSecondsToTimeString )
} }}">
{{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }}
</div>
</mat-slide-toggle>
</div>
</div>
<tb-calculate-field-output formControlName="output"
containerInputClass="grid items-start grid-cols-2 gap-3 xs:grid-cols-1"

54
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts

@ -37,7 +37,7 @@ import {
} from '@shared/models/calculated-field.models';
import { filter, map } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AVG_MONTH, AVG_QUARTER, DAY, HOUR, MINUTE, SECOND, YEAR } from '@shared/models/time/time.models';
import { AVG_MONTH, AVG_QUARTER, DAY, HOUR, MINUTE, SECOND, WEEK, YEAR } from '@shared/models/time/time.models';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
@ -88,6 +88,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
@Input() testScript: (expression?: string) => Observable<string>;
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<CalculatedFieldOutput>(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');

12
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html

@ -16,7 +16,9 @@
-->
<div class="flex flex-col gap-3">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col" [class.arguments-table-with-error]="errorText">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col"
[class.arguments-table-with-error]="errorText"
[class.!hidden]="!(dataSource.isEmpty() | async) === false">
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
@ -121,14 +123,14 @@
*matHeaderRowDef="['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-header-row>
<mat-row *matRowDef="let argument; columns: ['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center">
{{ 'calculated-fields.no-zone-configured' | translate }}
</div>
@if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center tb-prompt required">
{{ 'calculated-fields.no-zone-configured' | translate }}
</div>
<div class="flex h-9 justify-between">
<button type="button"
mat-stroked-button

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts

@ -195,8 +195,6 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
private updateErrorText(): void {
if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) {
this.errorText = 'calculated-fields.hint.geofencing-entity-not-found';
} else if (!this.zoneGroupsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.geofencing-empty';
} else {
this.errorText = '';
}

15
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html

@ -16,7 +16,9 @@
-->
<div class="flex flex-col gap-3">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col" [class.arguments-table-with-error]="errorText">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col"
[class.arguments-table-with-error]="errorText"
[class.!hidden]="!(dataSource.isEmpty() | async) === false">
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
@ -98,13 +100,10 @@
<mat-header-row class="mat-row-select" *matHeaderRowDef=displayColumns></mat-header-row>
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center">
{{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
</div>
@if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center required">
{{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
</div>
<div class="flex h-9 justify-between">
<button type="button"

16
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html

@ -72,12 +72,14 @@
</tb-time-unit-input>
</div>
<tb-calculate-field-output formControlName="output" [entityId]="entityId" simpleMode hiddenName>
<div class="tb-form-row simpleMode flex-1">
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.use-latest-timestamp</div>
</div>
</mat-slide-toggle>
</div>
@if (relatedAggregationConfiguration.get('output').value?.type === OutputType.Timeseries) {
<div class="tb-form-row simpleMode flex-1">
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.use-latest-timestamp</div>
</div>
</mat-slide-toggle>
</div>
}
</tb-calculate-field-output>
</div>

1
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" />
</div>
<div class="tb-form-panel no-gap">

44
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -368,6 +368,34 @@
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.intermediate-aggregation-interval</mat-label>
<input matInput required min="0" step="1"
formControlName="intermediateAggregationIntervalInSecForCF"
type="number">
<mat-error *ngIf="tenantProfileConfigurationForm.get('intermediateAggregationIntervalInSecForCF').hasError('required')">
{{ 'tenant-profile.intermediate-aggregation-interval-required' | translate}}
</mat-error>
<mat-error *ngIf="tenantProfileConfigurationForm.get('intermediateAggregationIntervalInSecForCF').hasError('min')">
{{ 'tenant-profile.intermediate-aggregation-interval-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.reevaluation-check-interval</mat-label>
<input matInput required min="0" step="1"
formControlName="cfReevaluationCheckInterval"
type="number">
<mat-error *ngIf="tenantProfileConfigurationForm.get('cfReevaluationCheckInterval').hasError('required')">
{{ 'tenant-profile.reevaluation-check-interval-required' | translate}}
</mat-error>
<mat-error *ngIf="tenantProfileConfigurationForm.get('cfReevaluationCheckInterval').hasError('min')">
{{ 'tenant-profile.reevaluation-check-interval-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.relation-search-entity-limit</mat-label>
@ -526,6 +554,22 @@
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div class="fields-element flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.alarms-reevaluation-interval</mat-label>
<input matInput required min="0" step="1"
formControlName="alarmsReevaluationInterval"
type="number">
<mat-error *ngIf="tenantProfileConfigurationForm.get('alarmsReevaluationInterval').hasError('required')">
{{ 'tenant-profile.alarms-reevaluation-interval-required' | translate}}
</mat-error>
<mat-error *ngIf="tenantProfileConfigurationForm.get('alarmsReevaluationInterval').hasError('min')">
{{ 'tenant-profile.alarms-reevaluation-interval-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<div class="flex-1"></div>
</div>
</fieldset>
<fieldset class="fields-group">

3
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: [''],

16
ui-ngx/src/app/modules/home/components/relation/relation-table.component.html

@ -104,8 +104,20 @@
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.type }}
<mat-cell *matCellDef="let relation" class="type">
<div class="flex flex-row" style="align-items: center">
<span class="ellipsis">{{ relation.type }}</span>
<tb-copy-button
class="type-copy"
[disabled]="isLoading$ | async"
[copyText]="relation.type"
tooltipText="{{ 'relation.copy-type' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
</div>
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toEntityTypeName">

11
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;
}
}
}
}

11
ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html

@ -15,8 +15,11 @@
limitations under the License.
-->
<div class="tb-two-factor-auth-login-content mat-app-background tb-dark flex flex-row items-center justify-center"
<div class="tb-two-factor-auth-login-content mat-app-background tb-dark flex flex-col items-center justify-center"
style="width: 100%;">
@if (isLoading$ | async) {
<mat-progress-bar color="warn" class="progress-bar" mode="indeterminate"></mat-progress-bar>
}
@switch (state()) {
@case (ForceTwoFAState.SETUP) {
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
@ -59,7 +62,7 @@
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="flex flex-col items-center justify-start">
<div tb-toast class="flex flex-col items-center justify-start">
<p class="mat-body qr-code-description mb-4" translate>login.scan-qr-code</p>
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
<p class="mat-body qr-code-description" translate>login.enter-key-manually</p>
@ -109,7 +112,7 @@
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="flex flex-col items-center justify-start">
<div tb-toast class="flex flex-col items-center justify-start">
<form [formGroup]="smsConfigForm" class="mb-12">
<p class="mat-body step-description input" translate>login.sms-description</p>
<div class="flex flex-row items-center justify-between gap-3.75">
@ -156,7 +159,7 @@
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="flex flex-col items-center justify-start">
<div tb-toast class="flex flex-col items-center justify-start">
<form [formGroup]="emailConfigForm" class="mb-8 w-full">
<p class="mat-body step-description input" translate>login.email-description</p>
<mat-form-field class="mat-block input-container flex-1">

6
ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss

@ -24,6 +24,12 @@
.tb-two-factor-auth-login-content {
background-color: #eee;
.progress-bar {
z-index: 10;
margin-bottom: -4px;
width: 450px;
}
.tb-two-factor-auth-login-card {
max-height: 100vh;
overflow: auto;

8
ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts

@ -39,6 +39,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import printTemplate from '@home/pages/security/authentication-dialog/backup-code-print-template.raw';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { mergeMap, tap } from 'rxjs/operators';
import { ActionNotificationShow } from "@core/notification/notification.actions";
enum ForceTwoFAState {
SETUP = 'setup',
@ -261,6 +262,13 @@ export class ForceTwoFactorAuthLoginComponent extends PageComponent implements O
this.configForm.get('verificationCode').setErrors({incorrectCode: true});
} else if (error.status === 429) {
this.configForm.get('verificationCode').setErrors({tooManyRequest: true});
} else {
this.store.dispatch(new ActionNotificationShow({
message: error.error.message,
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left'
}));
}
}
})

4
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -475,4 +475,8 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
get showEntityLink(): boolean {
return this.selectEntityFormGroup.get('entity').value && this.disabled && this.entityURL !== '';
}
markAsTouched(): void {
this.selectEntityFormGroup.get('entity').markAsTouched();
}
}

4
ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts

@ -173,4 +173,8 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
return '';
}
}
markAsTouched(): void {
this.entityTypeFormGroup.get('entityType').markAsTouched();
}
}

8
ui-ngx/src/app/shared/components/string-autocomplete.component.html

@ -31,21 +31,21 @@
</button>
<mat-icon *ngIf="selectionFormControl.errors && selectionFormControl.touched && !showInlineError"
matIconSuffix
[matTooltip]="errorText"
[matTooltip]="getErrorMessage"
matTooltipPosition="above"
[matTooltipClass]="tooltipClass"
class="material-icons tb-suffix-show-always tb-error">
warning
</mat-icon>
<mat-error *ngIf="selectionFormControl.errors && showInlineError">
{{errorText}}
{{ getErrorMessage }}
</mat-error>
<mat-autocomplete
#optionsAutocomplete="matAutocomplete"
class="tb-autocomplete tb-options-input-autocomplete"
class="tb-autocomplete"
[panelWidth]="panelWidth">
<mat-option *ngFor="let option of filteredOptions$ | async" [value]="option">
<span class="tb-option-name flex-1" [innerHTML]="option | highlight:searchText:true:'ig'"></span>
<span [innerHTML]="option | highlight:searchText:true:'ig'"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

20
ui-ngx/src/app/shared/components/string-autocomplete.component.scss

@ -25,25 +25,5 @@
margin-right: 8px;
}
}
.tb-autocomplete.tb-option-input-autocomplete {
.mat-mdc-option {
border-bottom: none;
.mdc-list-item__primary-text {
flex: 1;
display: flex;
flex-direction: row;
gap: 8px;
.tb-option {
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
}
}
}
}
}
}

50
ui-ngx/src/app/shared/components/string-autocomplete.component.ts

@ -14,27 +14,25 @@
/// limitations under the License.
///
import {
Component,
Input,
forwardRef,
OnInit,
ViewChild,
ElementRef
} from '@angular/core';
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
FormBuilder,
FormControl,
Validators,
FormBuilder
NG_VALUE_ACCESSOR,
ValidatorFn,
Validators
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { tap, map, switchMap, take } from 'rxjs/operators';
import { map, switchMap, take, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
export interface ErrorMessageConfig {
[errorKey: string]: string;
}
@Component({
selector: 'tb-string-autocomplete',
templateUrl: './string-autocomplete.component.html',
@ -85,6 +83,12 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
errorText: string;
@Input()
controlValidators: ValidatorFn[] = [];
@Input()
errorMessages: ErrorMessageConfig;
@Input()
@coerceBoolean()
showInlineError = false;
@ -107,7 +111,13 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit
ngOnInit() {
const validators = [Validators.pattern(/.*\S.*/)];
if (this.required) {
if (this.controlValidators?.length) {
validators.push(...this.controlValidators);
const parentHasRequired = this.controlValidators.some(v => v === Validators.required);
if (this.required && !parentHasRequired) {
validators.push(Validators.required);
}
} else if (this.required) {
validators.push(Validators.required);
}
this.selectionFormControl = this.fb.control('', validators);
@ -184,4 +194,18 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit
this.nameInput.nativeElement.focus();
}, 0);
}
get getErrorMessage(): string {
if (!this.selectionFormControl.errors) {
return '';
}
if (this.errorMessages) {
for (const errorKey in this.selectionFormControl.errors) {
if (this.errorMessages[errorKey]) {
return this.errorMessages[errorKey];
}
}
}
return this.errorText;
}
}

4
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -183,8 +183,8 @@ export class ImportExportService {
});
}
public openCalculatedFieldImportDialog(): Observable<CalculatedField> {
return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe(
public openCalculatedFieldImportDialog(importTitle = 'calculated-fields.import', importFileLabel = 'calculated-fields.file'): Observable<CalculatedField> {
return this.openImportDialog(importTitle, importFileLabel).pipe(
catchError(() => of(null)),
);
}

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

@ -18,17 +18,10 @@
import { CustomTimeSchedulerItem } from "@shared/models/device.models";
import { DashboardId } from "@shared/models/id/dashboard-id";
import { TimeUnit } from "@shared/models/time/time.models";
import {
BooleanOperation,
ComplexOperation,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
StringOperation
} from "@shared/models/query/query.models";
import { ComplexOperation, EntityKeyValueType, FilterPredicateType } from "@shared/models/query/query.models";
import { EntityType } from "@shared/models/entity-type.models";
import { Observable } from "rxjs";
import { CalculatedField } from "@shared/models/calculated-field.models";
import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export enum AlarmRuleScheduleType {
ANY_TIME = 'ANY_TIME',
@ -250,3 +243,62 @@ export const alarmRuleDefaultScript =
'return temperature > 20;'
export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, expression: string, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>;
export function checkPredicates(predicates: any[], validSet: Set<string>): boolean {
for (const predicate of predicates) {
if (!predicate) continue;
if (predicate?.value?.dynamicValueArgument) {
if (!validSet.has(predicate.value.dynamicValueArgument)) {
return false;
}
}
if (predicate.type === 'COMPLEX' && Array.isArray(predicate.predicates)) {
if (!checkPredicates(predicate.predicates, validSet)) {
return false;
}
}
}
return true;
}
export function areFilterAndPredicateArgumentsValid(obj: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(Object.keys(args));
const filter = obj || [];
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates, validSet)) {
return false;
}
}
return true;
}
export function areFiltersAndPredicateArgumentsValid(obj: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(Object.keys(args));
const filters = obj || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates, validSet)) {
return false;
}
}
}
return true;
}
export function isPredicateArgumentsValid(predicates: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(Object.keys(args));
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates, validSet)) {
return false;
}
}
return true;
}

1
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -168,6 +168,7 @@ export interface CalculatedFieldEntityAggregationConfiguration {
metrics: Record<string, CalculatedFieldAggMetric>;
interval: AggInterval;
watermark?: WatermarkConfig;
produceIntermediateResult?: boolean;
output: CalculatedFieldOutput & { decimalsByDefault?: number; };
}

9
ui-ngx/src/app/shared/models/tenant.model.ts

@ -111,6 +111,10 @@ export interface DefaultTenantProfileConfiguration {
minAllowedAggregationIntervalInSecForCF: number;
maxRelatedEntitiesToReturnPerCfArgument: number;
minAllowedScheduledUpdateIntervalInSecForCF: number;
intermediateAggregationIntervalInSecForCF: number;
cfReevaluationCheckInterval: number;
alarmsReevaluationInterval: number;
maxDataPointsPerRollingArg: number;
maxStateSizeInKBytes: number;
maxSingleValueArgumentSizeInKBytes: number;
@ -176,10 +180,13 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxArgumentsPerCF: 10,
maxDataPointsPerRollingArg: 1000,
maxRelationLevelPerCfArgument: 10,
minAllowedDeduplicationIntervalInSecForCF: 60,
minAllowedDeduplicationIntervalInSecForCF: 10,
minAllowedAggregationIntervalInSecForCF: 60,
maxRelatedEntitiesToReturnPerCfArgument: 100,
minAllowedScheduledUpdateIntervalInSecForCF: 0,
intermediateAggregationIntervalInSecForCF: 300,
cfReevaluationCheckInterval: 60,
alarmsReevaluationInterval: 60,
maxStateSizeInKBytes: 32,
maxSingleValueArgumentSizeInKBytes: 2,
calculatedFieldDebugEventsRateLimit: ''

2
ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md

@ -49,7 +49,7 @@ The argument value for a specific time schedule must be a JSON object in the fol
```javascript
{
"type": "CUSTOM"
"type": "CUSTOM",
"timezone": "Europe/Kiev",
"items": [
{

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

@ -1122,7 +1122,7 @@
"add-argument": "Add argument",
"test-script-function": "Test script function",
"test-expression-function": "Test expression function",
"no-arguments": "No arguments configured",
"no-arguments": "At least one argument is required.",
"argument-settings": "Argument settings",
"argument-current": "Current entity",
"argument-current-tenant": "Current tenant",
@ -1176,7 +1176,7 @@
"target-zone": "Target zone",
"perimeter-key": "Perimeter key",
"report-strategy": "Report strategy",
"no-zone-configured": "No zone group configured",
"no-zone-configured": "At least one zone is required.",
"no-zone-configured-required": "At least one zone group must be configured.",
"add-zone-group": "Add zone group",
"report-transition-event-only": "Transition events only",
@ -1245,7 +1245,7 @@
"key": "Key",
"function": "Function"
},
"no-metrics-configured": "No metrics configured",
"no-metrics-configured": "At least one metric is required.",
"add-metric": "Add metric",
"max-metrics": "Maximum number of metrics reached.",
"metric-settings": "Metric settings",
@ -1268,7 +1268,7 @@
"ttl-min": "Only 0 minimum TTL is allowed",
"processing-parameters": "Processing parameters",
"hint": {
"strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing",
"strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing.",
"processing-options": "Processing options",
"update-attribute-only-on-value-change": "Updates attribute on every incoming message, regardless of whether the value has changed. This increases API usage and reduces performance.",
"update-attribute-only-on-value-change-enabled": "Updates attribute only when the value changes. If the value is unchanged, timestamps are not updated and notifications are not sent.",
@ -1316,7 +1316,9 @@
"duration": "Duration",
"duration-required": "Duration is required",
"duration-min": "Duration should be at least 1 minute",
"duration-hint": "How long to wait for delayed data after the interval ends"
"duration-hint": "How long to wait for delayed data after the interval ends",
"produce-intermediate-result": "Produce intermediate result",
"produce-intermediate-result-hint": "Calculates metrics during the current interval to produce an intermediate result. Updates occur no more often than once every {{ time }}."
},
"hint": {
"arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.",
@ -1380,7 +1382,7 @@
"alarm-rules-old": "Old",
"alarm-rules-actual": "Actual",
"severities": "Severities",
"cleared": "Clearing condition",
"cleared": "Clear condition",
"delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?",
"delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.",
"delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?",
@ -1392,6 +1394,7 @@
"list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }",
"selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected",
"import": "Import alarm rule",
"file": "Alarm rule file",
"export": "Export alarm rule",
"export-failed-error": "Unable to export alarm rule: {{error}}",
"entity-type": "Entity type",
@ -1400,6 +1403,7 @@
"target-entity": "Target entity",
"target-entities": "Target entities",
"alarm-type": "Alarm type",
"alarm-type-hint": "Unique identifier (e.g., HighTemperatureAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.",
"alarm-type-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.",
"alarm-type-max-length": "Alarm type should be less than 256 characters.",
@ -1435,8 +1439,7 @@
"add-filter": "Add argument filter",
"edit-filter": "Argument filter",
"remove-filter": "Remove argument filter",
"no-filter": "No argument filters configured",
"filter-required": "At least one filter must be configured.",
"no-filter": "At least one filter is required.",
"conditions": {
"simple": "Simple",
"duration": "Duration",
@ -1495,9 +1498,9 @@
"condition-type": "Condition type",
"condition-type-hint": "\"Duration\" and \"Repeating\" options are not available when the \"Missing for\" operation is used in the filter.",
"select-alarm-severity": "Select alarm severity",
"add-create-alarm-rule-prompt": "At least one creation condition should be configured",
"add-create-alarm-rule": "Add creation condition",
"add-clear-alarm-rule": "Add clearing condition",
"add-create-alarm-rule-prompt": "At least one trigger condition is required.",
"add-create-alarm-rule": "Add trigger condition",
"add-clear-alarm-rule": "Add clear condition",
"condition-duration": "Condition duration",
"condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit",
@ -1510,9 +1513,9 @@
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
"create-conditions": "Creation conditions",
"clear-condition": "Clearing condition",
"no-clear-alarm-rule": "Clearing condition not configured",
"create-conditions": "Alarm Trigger Conditions",
"clear-condition": "Alarm Clear Condition",
"no-clear-alarm-rule": "No clear condition configured.",
"advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm to related entities",
"alarm-rule-relation-types-list": "Relation types",
@ -4981,7 +4984,8 @@
"additional-info": "Additional info (JSON)",
"invalid-additional-info": "Unable to parse additional info json.",
"no-relations-text": "No relations found",
"not": "Not"
"not": "Not",
"copy-type": "Copy type"
},
"resource": {
"add": "Add resource",
@ -6273,6 +6277,15 @@
"min-allowed-deduplication-interval": "Min allowed deduplication interval (seconds)",
"min-allowed-deduplication-interval-range": "Min allowed deduplication interval value can't be negative",
"min-allowed-deduplication-interval-required": "Min allowed deduplication interval is required",
"intermediate-aggregation-interval": "Intermediate aggregation interval (seconds)",
"intermediate-aggregation-interval-range": "Intermediate aggregation interval value can't be less than '1'",
"intermediate-aggregation-interval-required": "Intermediate aggregation interval is required",
"reevaluation-check-interval": "Reevaluation check interval (seconds)",
"reevaluation-check-interval-range": "Reevaluation check interval value can't be less than '1'",
"reevaluation-check-interval-required": "Reevaluation check interval is required",
"alarms-reevaluation-interval": "Alarms reevaluation interval (seconds)",
"alarms-reevaluation-interval-range": "Alarms reevaluation interval value can't be less than '1'",
"alarms-reevaluation-interval-required": "Alarms reevaluation interval is required",
"min-allowed-aggregation-interval": "Min allowed aggregation interval (seconds)",
"min-allowed-aggregation-interval-range": "Min allowed aggregation interval value can't be negative",
"min-allowed-aggregation-interval-required": "Min allowed aggregation interval is required",

Loading…
Cancel
Save