Browse Source

Merge pull request #14787 from thingsboard/rc

RC
pull/14973/head
Viacheslav Klimov 9 months ago
committed by GitHub
parent
commit
025d6f0bb5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 51
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  2. 1
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  3. 15
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  4. 10
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  5. 28
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  6. 18
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java
  7. 27
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java
  8. 13
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  9. 12
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java
  10. 7
      application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java
  11. 1
      application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java
  12. 4
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java
  13. 4
      application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java
  14. 101
      application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java
  15. 2
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  16. 9
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  17. 3
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java
  18. 3
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java
  19. 28
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
  20. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java
  21. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  22. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java
  23. 12
      dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
  24. 60
      dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
  25. 19
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  26. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
  27. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
  28. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  29. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
  30. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
  31. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java
  32. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java
  33. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
  34. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java
  35. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java
  36. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java
  37. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  38. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java
  39. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java
  40. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java
  41. 8
      dao/src/main/resources/sql/schema-ts-psql.sql
  42. 1
      ui-ngx/src/app/core/auth/auth.models.ts
  43. 1
      ui-ngx/src/app/core/auth/auth.reducer.ts
  44. 9
      ui-ngx/src/app/core/services/calculated-field-form.service.ts
  45. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html
  46. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts
  47. 11
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  48. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  49. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html
  50. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  51. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  52. 7
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
  53. 14
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  54. 7
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  55. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  56. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  57. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  58. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
  59. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html
  60. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
  61. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  62. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  63. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html
  64. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  65. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  66. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  67. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  68. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
  69. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  70. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html
  71. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  72. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html
  73. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  74. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  75. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  76. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html
  77. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  78. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  79. 3
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  80. 2
      ui-ngx/src/app/shared/components/time-unit-input.component.html
  81. 15
      ui-ngx/src/app/shared/components/time-unit-input.component.ts
  82. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  83. 2
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json

51
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -262,17 +262,35 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) { private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) {
checkCfIntervalForUpdate();
long maxRelatedEntitiesPerCfArgument = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument);
List<CalculatedFieldCtx> cfsToReinit = new ArrayList<>();
Stream.concat(
calculatedFields.values().stream(),
entityIdCalculatedFields.values().stream().flatMap(Collection::stream)
).forEach(ctx -> {
if (ctx.hasRelatedEntities() && ctx.getMaxRelatedEntitiesPerCfArgument() != maxRelatedEntitiesPerCfArgument) {
cfsToReinit.add(ctx);
}
ctx.setTenantProfileProperties();
});
if (!cfsToReinit.isEmpty()) {
MultipleTbCallback cfsReinitCallback = new MultipleTbCallback(cfsToReinit.size(), callback);
cfsToReinit.forEach(ctx -> applyToTargetCfEntityActors(ctx, cfsReinitCallback, (id, cb) -> initCfForEntity(id, ctx, StateAction.REINIT, cb)));
} else {
callback.onSuccess();
}
}
private void checkCfIntervalForUpdate() {
long updatedCfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); long updatedCfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval);
if (cfCheckInterval != updatedCfCheckInterval) { if (cfCheckInterval != updatedCfCheckInterval) {
cfCheckInterval = updatedCfCheckInterval; cfCheckInterval = updatedCfCheckInterval;
cancelReevaluationTask(); cancelReevaluationTask();
scheduleCfsReevaluation(); scheduleCfsReevaluation();
} }
Stream.concat(
calculatedFields.values().stream(),
entityIdCalculatedFields.values().stream().flatMap(Collection::stream)
).forEach(CalculatedFieldCtx::setTenantProfileProperties);
callback.onSuccess();
} }
private void onEntityCreated(ComponentLifecycleMsg msg, TbCallback callback) { private void onEntityCreated(ComponentLifecycleMsg msg, TbCallback callback) {
@ -383,10 +401,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
.toList(); .toList();
MultipleTbCallback directionCallback = new MultipleTbCallback(matchingCfs.size(), parentCallback); MultipleTbCallback directionCallback = new MultipleTbCallback(matchingCfs.size(), parentCallback);
matchingCfs.forEach(ctx -> relationAction.accept(mainId, ctx, directionCallback));
matchingCfs.forEach(ctx ->
applyToTargetCfEntityActors(ctx, directionCallback, (entityId, cb) -> relationAction.accept(entityId, ctx, cb))
);
} }
private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException {
@ -580,18 +595,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
Predicate<EntityId> matchesCfEntity = relatedEntity -> cfEntityId.equals(relatedEntity) || cfEntityId.equals(getProfileId(tenantId, relatedEntity)); Predicate<EntityId> matchesCfEntity = relatedEntity -> cfEntityId.equals(relatedEntity) || cfEntityId.equals(getProfileId(tenantId, relatedEntity));
if (byRelationPathQuery != null && !byRelationPathQuery.isEmpty()) { if (byRelationPathQuery != null && !byRelationPathQuery.isEmpty()) {
switch (relation.direction()) { switch (relation.direction()) {
case FROM -> { case FROM -> byRelationPathQuery.stream()
EntityRelation entityRelation = byRelationPathQuery.get(0); // only one supported .filter(entityRelation -> matchesCfEntity.test(entityRelation.getFrom()))
EntityId relatedId = entityRelation.getFrom(); .forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getFrom())));
if (matchesCfEntity.test(relatedId)) { case TO -> byRelationPathQuery.stream()
result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), relatedId)); .filter(entityRelation -> matchesCfEntity.test(entityRelation.getTo()))
} .forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getTo())));
}
case TO -> {
byRelationPathQuery.stream()
.filter(entityRelation -> matchesCfEntity.test(entityRelation.getTo()))
.forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getTo())));
}
} }
} }
} }

1
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -164,6 +164,7 @@ public class SystemInfoController extends BaseController {
systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg());
systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF()); systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF());
systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument());
systemParams.setMaxRelatedEntitiesToReturnPerCfArgument(tenantProfileConfiguration.getMaxRelatedEntitiesToReturnPerCfArgument());
systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF());
systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF());
systemParams.setIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getIntermediateAggregationIntervalInSecForCF()); systemParams.setIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getIntermediateAggregationIntervalInSecForCF());

15
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -169,7 +169,7 @@ public class TenantController extends BaseController {
return checkNotNull(tenantService.findTenantInfos(pageLink)); return checkNotNull(tenantService.findTenantInfos(pageLink));
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/tenants", params = {"tenantIds"}) @GetMapping(value = "/tenants", params = {"tenantIds"})
public List<Tenant> getTenantsByIds( public List<Tenant> getTenantsByIds(
@Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@ -179,7 +179,18 @@ public class TenantController extends BaseController {
for (UUID tenantIdUUID : tenantUUIDs) { for (UUID tenantIdUUID : tenantUUIDs) {
tenantIds.add(TenantId.fromUUID(tenantIdUUID)); tenantIds.add(TenantId.fromUUID(tenantIdUUID));
} }
return tenantService.findTenantsByIds(tenantId, tenantIds); List<Tenant> tenants = tenantService.findTenantsByIds(tenantId, tenantIds);
return filterTenantsByReadPermission(tenants);
}
private List<Tenant> filterTenantsByReadPermission(List<Tenant> tenants) {
return tenants.stream().filter(tenant -> {
try {
return accessControlService.hasPermission(getCurrentUser(), Resource.TENANT, Operation.READ, tenant.getId(), tenant);
} catch (ThingsboardException e) {
return false;
}
}).toList();
} }
} }

10
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -247,14 +247,8 @@ public abstract class AbstractCalculatedFieldProcessingService {
} }
return switch (relation.direction()) { return switch (relation.direction()) {
case FROM -> relations.stream() case FROM -> relations.stream().map(EntityRelation::getTo).toList();
.map(EntityRelation::getTo) case TO -> relations.stream().map(EntityRelation::getFrom).toList();
.toList();
case TO -> relations.stream()
.map(EntityRelation::getFrom)
.findFirst()
.map(List::of)
.orElseGet(Collections::emptyList);
}; };
}, calculatedFieldCallbackExecutor); }, calculatedFieldCallbackExecutor);
} }

28
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -29,6 +29,7 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldReevaluateMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldReevaluateMsg;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule; import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
@ -54,11 +55,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.relation.RelationService; 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.dao.util.TimeUtils;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
@ -129,6 +128,7 @@ public class CalculatedFieldCtx implements Closeable {
private long scheduledUpdateIntervalMillis; private long scheduledUpdateIntervalMillis;
private long cfCheckReevaluationIntervalMillis; private long cfCheckReevaluationIntervalMillis;
private long alarmReevaluationIntervalMillis; private long alarmReevaluationIntervalMillis;
private long maxRelatedEntitiesPerCfArgument;
private Argument propagationArgument; private Argument propagationArgument;
private boolean applyExpressionForResolvedArguments; private boolean applyExpressionForResolvedArguments;
@ -301,12 +301,18 @@ public class CalculatedFieldCtx implements Closeable {
} }
public void setTenantProfileProperties() { public void setTenantProfileProperties() {
ApiLimitService apiLimitService = systemContext.getApiLimitService(); TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenantId);
this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; if (tenantProfile == null) {
this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; throw new IllegalStateException("Tenant Profile not found for tenant: " + tenantId);
this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getIntermediateAggregationIntervalInSecForCF)); }
this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval)); tenantProfile.getProfileConfiguration().ifPresent(config -> {
this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval)); this.maxStateSize = config.getMaxStateSizeInKBytes() * 1024L;
this.maxSingleValueArgumentSize = config.getMaxSingleValueArgumentSizeInKBytes() * 1024L;
this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getIntermediateAggregationIntervalInSecForCF());
this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getCfReevaluationCheckInterval());
this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getAlarmsReevaluationInterval());
this.maxRelatedEntitiesPerCfArgument = config.getMaxRelatedEntitiesToReturnPerCfArgument();
});
} }
public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) {
@ -756,6 +762,12 @@ public class CalculatedFieldCtx implements Closeable {
return scheduledUpdateIntervalMillis == DISABLED_INTERVAL_VALUE; return scheduledUpdateIntervalMillis == DISABLED_INTERVAL_VALUE;
} }
public boolean hasRelatedEntities() {
return CalculatedFieldType.GEOFENCING == cfType
|| CalculatedFieldType.PROPAGATION == cfType
|| CalculatedFieldType.RELATED_ENTITIES_AGGREGATION == cfType;
}
public boolean shouldFetchRelatedEntities(CalculatedFieldState state) { public boolean shouldFetchRelatedEntities(CalculatedFieldState state) {
if (!cfHasRelationPathQuerySource) { if (!cfHasRelationPathQuerySource) {
return false; return false;

18
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInp
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
@ -300,8 +301,25 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
if (argumentEntry == null || argumentEntry.isEmpty()) { if (argumentEntry == null || argumentEntry.isEmpty()) {
return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR); return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR);
} }
if (argumentEntry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) {
try {
checkConstraintByDirection(relatedEntitiesArgumentEntry);
} catch (Exception e) {
return ReadinessStatus.notReady(e.getMessage());
}
}
} }
return ReadinessStatus.READY; return ReadinessStatus.READY;
} }
public void checkConstraintByDirection(RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) {
if (ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) {
if (EntitySearchDirection.TO == config.getRelation().direction()) {
if (relatedEntitiesArgumentEntry.getEntityInputs().size() > 1) {
throw new IllegalArgumentException("More than one related entity is not supported for relation direction 'TO'. Found: " + relatedEntitiesArgumentEntry.getEntityInputs().size() + ".");
}
}
}
}
} }

27
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java

@ -66,23 +66,34 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs
@Override @Override
public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) {
checkRelatedEntitiesNumber(ctx);
entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs);
return true;
} else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { } else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
if (entry.isForceResetPrevious()) { if (entry.isForceResetPrevious()) {
checkRelatedEntitiesNumber(ctx);
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry);
return true;
}
ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId());
if (argumentEntry != null) {
argumentEntry.updateEntry(singleValueArgumentEntry, ctx);
} else { } else {
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId());
if (argumentEntry != null) {
argumentEntry.updateEntry(singleValueArgumentEntry, ctx);
} else {
checkRelatedEntitiesNumber(ctx);
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry);
}
} }
return true;
} else { } else {
throw new IllegalArgumentException("Unsupported argument entry type for aggregation argument entry: " + entry.getType()); throw new IllegalArgumentException("Unsupported argument entry type for aggregation argument entry: " + entry.getType());
} }
return true;
}
private void checkRelatedEntitiesNumber(CalculatedFieldCtx ctx) {
if (entityInputs.size() >= ctx.getMaxRelatedEntitiesPerCfArgument()) {
throw new IllegalArgumentException(
"Exceeded the maximum allowed related entities per argument '"
+ ctx.getMaxRelatedEntitiesPerCfArgument() + "'. Increase the limit in the tenant profile configuration."
);
}
} }
@Override @Override

13
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java

@ -68,6 +68,8 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
private CalculatedFieldProcessingService cfProcessingService; private CalculatedFieldProcessingService cfProcessingService;
private long now;
public EntityAggregationCalculatedFieldState(EntityId entityId) { public EntityAggregationCalculatedFieldState(EntityId entityId) {
super(entityId); super(entityId);
} }
@ -100,7 +102,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx) throws Exception { public ListenableFuture<CalculatedFieldResult> performCalculation(Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx) throws Exception {
createIntervalIfNotExist(); createIntervalIfNotExist();
long now = System.currentTimeMillis(); now = System.currentTimeMillis();
if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) {
if (debugTracker == null) { if (debugTracker == null) {
@ -114,7 +116,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
Map<AggIntervalEntry, Map<String, ArgumentEntry>> results = new HashMap<>(); Map<AggIntervalEntry, Map<String, ArgumentEntry>> results = new HashMap<>();
List<AggIntervalEntry> expiredIntervals = new ArrayList<>(); List<AggIntervalEntry> expiredIntervals = new ArrayList<>();
getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { getIntervals().forEach((intervalEntry, argIntervalStatuses) -> {
processInterval(now, intervalEntry, argIntervalStatuses, expiredIntervals, results); processInterval(intervalEntry, argIntervalStatuses, expiredIntervals, results);
}); });
removeExpiredIntervals(expiredIntervals); removeExpiredIntervals(expiredIntervals);
@ -202,8 +204,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
return intervals; return intervals;
} }
private void processInterval(long now, private void processInterval(AggIntervalEntry intervalEntry,
AggIntervalEntry intervalEntry,
Map<String, AggIntervalEntryStatus> args, Map<String, AggIntervalEntryStatus> args,
List<AggIntervalEntry> expiredIntervals, List<AggIntervalEntry> expiredIntervals,
Map<AggIntervalEntry, Map<String, ArgumentEntry>> results) { Map<AggIntervalEntry, Map<String, ArgumentEntry>> results) {
@ -244,11 +245,11 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
args.forEach((argName, argEntryIntervalStatus) -> { args.forEach((argName, argEntryIntervalStatus) -> {
if (argEntryIntervalStatus.intervalPassed(cfCheckInterval)) { if (argEntryIntervalStatus.intervalPassed(cfCheckInterval)) {
if (argEntryIntervalStatus.argsUpdated()) { if (argEntryIntervalStatus.argsUpdated()) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastMetricsEvalTs(now);
argEntryIntervalStatus.setLastArgsRefreshTs(DEFAULT_LAST_UPDATE_TS); argEntryIntervalStatus.setLastArgsRefreshTs(DEFAULT_LAST_UPDATE_TS);
processArgument(intervalEntry, argName, false, results); processArgument(intervalEntry, argName, false, results);
} else if (argEntryIntervalStatus.getLastMetricsEvalTs() == DEFAULT_LAST_UPDATE_TS) { } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == DEFAULT_LAST_UPDATE_TS) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastMetricsEvalTs(now);
processArgument(intervalEntry, argName, true, results); processArgument(intervalEntry, argName, true, results);
} }
} }

12
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java

@ -65,7 +65,7 @@ public class PropagationArgumentEntry implements ArgumentEntry {
throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType());
} }
if (updated.getAdded() != null) { if (updated.getAdded() != null) {
return checkAdded(updated.getAdded()); return checkAdded(updated.getAdded(), ctx);
} }
if (updated.getRemoved() != null) { if (updated.getRemoved() != null) {
return entityIds.remove(updated.getRemoved()); return entityIds.remove(updated.getRemoved());
@ -80,7 +80,7 @@ public class PropagationArgumentEntry implements ArgumentEntry {
return true; return true;
} }
boolean retained = entityIds.retainAll(dbEntityIds); boolean retained = entityIds.retainAll(dbEntityIds);
boolean added = checkAdded(dbEntityIds); boolean added = checkAdded(dbEntityIds, ctx);
return retained || added; return retained || added;
} }
if (updated.isEmpty()) { if (updated.isEmpty()) {
@ -91,8 +91,14 @@ public class PropagationArgumentEntry implements ArgumentEntry {
return true; return true;
} }
private boolean checkAdded(Collection<EntityId> updatedIds) { private boolean checkAdded(Collection<EntityId> updatedIds, CalculatedFieldCtx ctx) {
for (EntityId id : updatedIds) { for (EntityId id : updatedIds) {
if (entityIds.size() >= ctx.getMaxRelatedEntitiesPerCfArgument()) {
throw new IllegalArgumentException(
"Exceeded the maximum allowed related entities per argument '"
+ ctx.getMaxRelatedEntitiesPerCfArgument() + "'. Increase the limit in the tenant profile configuration."
);
}
if (entityIds.add(id)) { if (entityIds.add(id)) {
if (added == null) { if (added == null) {
added = new ArrayList<>(); added = new ArrayList<>();

7
application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java

@ -17,10 +17,13 @@ package org.thingsboard.server.service.housekeeper.processor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import java.util.Arrays;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class EventsDeletionTaskProcessor extends HousekeeperTaskProcessor<HousekeeperTask> { public class EventsDeletionTaskProcessor extends HousekeeperTaskProcessor<HousekeeperTask> {
@ -29,7 +32,9 @@ public class EventsDeletionTaskProcessor extends HousekeeperTaskProcessor<Housek
@Override @Override
public void process(HousekeeperTask task) throws Exception { public void process(HousekeeperTask task) throws Exception {
eventService.removeEvents(task.getTenantId(), task.getEntityId(), null, 0L, System.currentTimeMillis()); // Only delete non-debug events for deleted entities.
EventType[] nonDebugEventTypes = Arrays.stream(EventType.values()).filter(eventType -> !eventType.isDebug()).toArray(EventType[]::new);
eventService.removeEvents(task.getTenantId(), task.getEntityId(), 0L, System.currentTimeMillis(), nonDebugEventTypes);
} }
@Override @Override

1
application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java

@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.edge.EdgeEventDao;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;

4
application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java

@ -68,9 +68,9 @@ public class CalculatedFieldArgumentUtils {
} }
public static ArgumentEntry createDefaultMetricArgumentEntry(String argKey, AggMetric metric) { public static ArgumentEntry createDefaultMetricArgumentEntry(String argKey, AggMetric metric) {
Long defaultValue = metric.getDefaultValue(); Double defaultValue = metric.getDefaultValue();
if (defaultValue != null) { if (defaultValue != null) {
return ArgumentEntry.createSingleValueArgument(new DoubleDataEntry(argKey, defaultValue.doubleValue())); return ArgumentEntry.createSingleValueArgument(new DoubleDataEntry(argKey, defaultValue));
} }
return new SingleValueArgumentEntry(); return new SingleValueArgumentEntry();
} }

4
application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java

@ -254,7 +254,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
AggMetric consumption = new AggMetric(); AggMetric consumption = new AggMetric();
consumption.setFunction(AggFunction.SUM); consumption.setFunction(AggFunction.SUM);
consumption.setInput(new AggKeyInput("en")); consumption.setInput(new AggKeyInput("en"));
consumption.setDefaultValue(9999L); consumption.setDefaultValue(9999.0);
aggMetrics.put("consumption", consumption); aggMetrics.put("consumption", consumption);
AggMetric avgEnergyConsumption = new AggMetric(); AggMetric avgEnergyConsumption = new AggMetric();
@ -319,7 +319,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
AggMetric consumption = new AggMetric(); AggMetric consumption = new AggMetric();
consumption.setFunction(AggFunction.SUM); consumption.setFunction(AggFunction.SUM);
consumption.setInput(new AggKeyInput("en")); consumption.setInput(new AggKeyInput("en"));
consumption.setDefaultValue(9999L); consumption.setDefaultValue(9999.0);
aggMetrics.put("consumption", consumption); aggMetrics.put("consumption", consumption);
AggMetric avgTemperature = new AggMetric(); AggMetric avgTemperature = new AggMetric();

101
application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java

@ -470,21 +470,47 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
@Test @Test
public void testCreateRelation_checkAggregation() throws Exception { public void testCreateRelation_checkAggregation() throws Exception {
createOccupancyCF(asset.getId()); Asset asset2 = createAsset("Asset 2", assetProfile.getId());
checkInitialCalculation(); Device device3 = createDevice("Device 3", "1234567890333");
Device device4 = createDevice("Device 4", "1234567890444");
Device device3 = createDevice("Device 3", deviceProfile.getId(), "1234567890333"); createEntityRelation(asset2.getId(), device3.getId(), "Contains");
createEntityRelation(asset2.getId(), device4.getId(), "Contains");
postTelemetry(device3.getId(), "{\"occupied\":true}"); createOccupancyCF(assetProfile.getId());
createEntityRelation(asset.getId(), device3.getId(), "Contains"); await().alias("create CF and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of(
"freeSpaces", "1",
"occupiedSpaces", "1",
"totalSpaces", "2"
));
await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) verifyTelemetry(asset2.getId(), Map.of(
"freeSpaces", "2",
"occupiedSpaces", "0",
"totalSpaces", "2"
));
});
Device device5 = createDevice("Device 5", "1234567890555");
createEntityRelation(asset2.getId(), device5.getId(), "Contains");
await().alias("create relation and perform aggregation on asset 2")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of( verifyTelemetry(asset.getId(), Map.of(
"freeSpaces", "1", "freeSpaces", "1",
"occupiedSpaces", "2", "occupiedSpaces", "1",
"totalSpaces", "2"
));
verifyTelemetry(asset2.getId(), Map.of(
"freeSpaces", "3",
"occupiedSpaces", "0",
"totalSpaces", "3" "totalSpaces", "3"
)); ));
}); });
@ -721,6 +747,43 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
}); });
} }
@Test
public void testUpdateMaxRelatedEntitiesPerArgument_checkAggregation() throws Exception {
loginSysAdmin();
updateDefaultTenantProfileConfig(tenantProfileConfig -> {
tenantProfileConfig.setMaxRelatedEntitiesToReturnPerCfArgument(1);
});
login("tenant@thingsboard.org", "testPassword");
createCountCF(asset.getId());
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode numberOfDevices = getLatestTelemetry(asset.getId(), "numberOfDevices");
assertThat(numberOfDevices).isNotNull();
assertThat(numberOfDevices.get("numberOfDevices").get(0).get("value").asText()).isEqualTo("1");
});
loginSysAdmin();
updateDefaultTenantProfileConfig(tenantProfileConfig -> {
tenantProfileConfig.setMaxRelatedEntitiesToReturnPerCfArgument(10);
});
login("tenant@thingsboard.org", "testPassword");
await().alias("update max related entities per argument and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode numberOfDevices = getLatestTelemetry(asset.getId(), "numberOfDevices");
assertThat(numberOfDevices).isNotNull();
assertThat(numberOfDevices.get("numberOfDevices").get(0).get("value").asText()).isEqualTo("2");
});
}
private void checkInitialCalculation() { private void checkInitialCalculation() {
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -832,6 +895,30 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
output); output);
} }
private CalculatedField createCountCF(EntityId entityId) {
Map<String, Argument> arguments = new HashMap<>();
Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("active", ArgumentType.TS_LATEST, null));
argument.setDefaultValue("true");
arguments.put("active", argument);
Map<String, AggMetric> aggMetrics = new HashMap<>();
AggMetric avgMetric = new AggMetric();
avgMetric.setFunction(AggFunction.COUNT);
avgMetric.setInput(new AggKeyInput("active"));
aggMetrics.put("numberOfDevices", avgMetric);
TimeSeriesOutput output = new TimeSeriesOutput();
output.setDecimalsByDefault(0);
return createAggCf("Number of devices", entityId,
new RelationPathLevel(EntitySearchDirection.FROM, "Contains"),
arguments,
aggMetrics,
output);
}
private CalculatedField createAggCf(String name, private CalculatedField createAggCf(String name,
EntityId entityId, EntityId entityId,
RelationPathLevel relation, RelationPathLevel relation,

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

@ -382,7 +382,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
AggMetric metric = new AggMetric(); AggMetric metric = new AggMetric();
metric.setInput(new AggKeyInput("en")); metric.setInput(new AggKeyInput("en"));
metric.setDefaultValue(9999L); metric.setDefaultValue(9999.0);
config.setMetrics(Map.of("consumption", metric)); config.setMetrics(Map.of("consumption", metric));
config.setWatermark(new Watermark(TimeUnit.DAYS.toSeconds(1))); config.setWatermark(new Watermark(TimeUnit.DAYS.toSeconds(1)));

9
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -219,6 +219,15 @@ public class TenantControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedTenant, foundTenant); Assert.assertEquals(savedTenant, foundTenant);
} }
loginTenantAdmin();
Tenant[] foundTenantsByTenant = doGet("/api/tenants?tenantIds=" + idsParam, Tenant[].class);
assertThat(foundTenantsByTenant).isEmpty();
Tenant[] foundCurrentTenant = doGet("/api/tenants?tenantIds=" + tenantId, Tenant[].class);
assertThat(foundCurrentTenant).hasSize(1);
assertThat(foundCurrentTenant[0].getTenantId()).isEqualTo(tenantId);
loginSysAdmin();
for (Tenant savedTenant : savedTenants) { for (Tenant savedTenant : savedTenants) {
deleteTenant(savedTenant.getId()); deleteTenant(savedTenant.getId());
} }

3
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java

@ -33,6 +33,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class PropagationArgumentEntryTest { public class PropagationArgumentEntryTest {
@ -48,6 +49,8 @@ public class PropagationArgumentEntryTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
lenient().when(ctx.getMaxRelatedEntitiesPerCfArgument()).thenReturn(1000L);
List<EntityId> propagationEntityIds = new ArrayList<>(); List<EntityId> propagationEntityIds = new ArrayList<>();
propagationEntityIds.add(ENTITY_1_ID); propagationEntityIds.add(ENTITY_1_ID);
propagationEntityIds.add(ENTITY_2_ID); propagationEntityIds.add(ENTITY_2_ID);

3
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java

@ -32,6 +32,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class RelatedEntitiesArgumentEntryTest { public class RelatedEntitiesArgumentEntryTest {
@ -48,6 +49,8 @@ public class RelatedEntitiesArgumentEntryTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
lenient().when(ctx.getMaxRelatedEntitiesPerCfArgument()).thenReturn(1000L);
Map<EntityId, ArgumentEntry> aggInputs = new HashMap<>(); Map<EntityId, ArgumentEntry> aggInputs = new HashMap<>();
aggInputs.put(device1, new SingleValueArgumentEntry(device1, new BasicTsKvEntry(ts - 100, new LongDataEntry("key", 12L), 1L))); aggInputs.put(device1, new SingleValueArgumentEntry(device1, new BasicTsKvEntry(ts - 100, new LongDataEntry("key", 12L), 1L)));
aggInputs.put(device2, new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 150, new LongDataEntry("key", 16L), 6L))); aggInputs.put(device2, new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 150, new LongDataEntry("key", 16L), 6L)));

28
application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.notification; package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -30,7 +31,12 @@ import org.thingsboard.server.common.data.notification.targets.NotificationTarge
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.notification.NotificationTargetDao; import org.thingsboard.server.dao.notification.NotificationTargetDao;
@ -175,6 +181,28 @@ public class NotificationTargetApiTest extends AbstractNotificationApiTest {
assertThat(error).containsIgnoringCase("referenced by scheduled notification request"); assertThat(error).containsIgnoringCase("referenced by scheduled notification request");
} }
@Test
public void findByTenantIdAndUsersFilterType() throws Exception {
loginSysAdmin();
NotificationTarget sysAdmins = createNotificationTarget(new SystemAdministratorsFilter());
NotificationTarget slack = new NotificationTarget();
slack.setName(RandomStringUtils.randomNumeric(5));
SlackNotificationTargetConfig slackConfig = new SlackNotificationTargetConfig();
SlackConversation slackConversation = new SlackConversation();
slackConversation.setType(SlackConversationType.DIRECT);
slackConversation.setId("U12345678");
slackConversation.setName("test");
slackConfig.setConversation(slackConversation);
slack.setConfiguration(slackConfig);
slack = saveNotificationTarget(slack);
List<NotificationTarget> sysAdminTargets = notificationTargetDao.findByTenantIdAndUsersFilterType(
TenantId.SYS_TENANT_ID, UsersFilterType.SYSTEM_ADMINISTRATORS
);
assertThat(sysAdminTargets).containsOnly(sysAdmins);
}
private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception { private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception {
return getErrorMessage(save(notificationTarget, statusMatcher)); return getErrorMessage(save(notificationTarget, statusMatcher));
} }

2
common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java

@ -41,6 +41,8 @@ public interface EventService {
void removeEvents(TenantId tenantId, EntityId entityId); void removeEvents(TenantId tenantId, EntityId entityId);
void removeEvents(TenantId tenantId, EntityId entityId, Long startTime, Long endTime, EventType... types);
void removeEvents(TenantId tenantId, EntityId entityId, EventFilter eventFilter, Long startTime, Long endTime); void removeEvents(TenantId tenantId, EntityId entityId, EventFilter eventFilter, Long startTime, Long endTime);
void cleanupEvents(long regularEventExpTs, long debugEventExpTs, boolean cleanupDb); void cleanupEvents(long regularEventExpTs, long debugEventExpTs, boolean cleanupDb);

1
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -40,6 +40,7 @@ public class SystemParams {
long maxDataPointsPerRollingArg; long maxDataPointsPerRollingArg;
int minAllowedScheduledUpdateIntervalInSecForCF; int minAllowedScheduledUpdateIntervalInSecForCF;
int maxRelationLevelPerCfArgument; int maxRelationLevelPerCfArgument;
int maxRelatedEntitiesToReturnPerCfArgument;
long minAllowedDeduplicationIntervalInSecForCF; long minAllowedDeduplicationIntervalInSecForCF;
long minAllowedAggregationIntervalInSecForCF; long minAllowedAggregationIntervalInSecForCF;
long intermediateAggregationIntervalInSecForCF; long intermediateAggregationIntervalInSecForCF;

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java

@ -27,6 +27,6 @@ public class AggMetric {
private AggFunction function; private AggFunction function;
private String filter; private String filter;
private AggInput input; private AggInput input;
private Long defaultValue; private Double defaultValue;
} }

12
dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java

@ -47,11 +47,6 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class BaseEventService implements EventService { public class BaseEventService implements EventService {
@Value("${sql.ttl.events.events_ttl:0}")
private long ttlInSec;
@Value("${sql.ttl.events.debug_events_ttl:604800}")
private long debugTtlInSec;
@Value("${event.debug.max-symbols:4096}") @Value("${event.debug.max-symbols:4096}")
private int maxDebugEventSymbols; private int maxDebugEventSymbols;
@ -125,7 +120,12 @@ public class BaseEventService implements EventService {
@Override @Override
public void removeEvents(TenantId tenantId, EntityId entityId) { public void removeEvents(TenantId tenantId, EntityId entityId) {
removeEvents(tenantId, entityId, null, null, null); removeEvents(tenantId, entityId, null, null);
}
@Override
public void removeEvents(TenantId tenantId, EntityId entityId, Long startTime, Long endTime, EventType... types) {
eventDao.removeEvents(tenantId.getId(), entityId.getId(), startTime, endTime, types);
} }
@Override @Override

60
dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java

@ -25,80 +25,24 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* The Interface EventDao.
*/
public interface EventDao { public interface EventDao {
/**
* Save or update event object async
*
* @param event the event object
* @return saved event object future
*/
ListenableFuture<Void> saveAsync(Event event); ListenableFuture<Void> saveAsync(Event event);
/**
* Find events by tenantId, entityId, eventType and pageLink.
*
* @param tenantId the tenantId
* @param entityId the entityId
* @param eventType the eventType
* @param pageLink the pageLink
* @return the event list
*/
PageData<? extends Event> findEvents(UUID tenantId, UUID entityId, EventType eventType, TimePageLink pageLink); PageData<? extends Event> findEvents(UUID tenantId, UUID entityId, EventType eventType, TimePageLink pageLink);
PageData<? extends Event> findEventByFilter(UUID tenantId, UUID entityId, EventFilter eventFilter, TimePageLink pageLink); PageData<? extends Event> findEventByFilter(UUID tenantId, UUID entityId, EventFilter eventFilter, TimePageLink pageLink);
/**
* Find latest events by tenantId, entityId and eventType.
*
* @param tenantId the tenantId
* @param entityId the entityId
* @param eventType the eventType
* @param limit the limit
* @return the event list
*/
List<? extends Event> findLatestEvents(UUID tenantId, UUID entityId, EventType eventType, int limit); List<? extends Event> findLatestEvents(UUID tenantId, UUID entityId, EventType eventType, int limit);
/**
* Find latest debug IN event by tenantId, entityId.
*
* @param tenantId the tenantId
* @param entityId the entityId
* @return the latest debug IN event
*/
Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId); Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId);
/**
* Executes stored procedure to cleanup old events. Uses separate ttl for debug and other events.
* @param regularEventExpTs the expiration time of the regular events
* @param debugEventExpTs the expiration time of the debug events
* @param cleanupDb
*/
void cleanupEvents(long regularEventExpTs, long debugEventExpTs, boolean cleanupDb); void cleanupEvents(long regularEventExpTs, long debugEventExpTs, boolean cleanupDb);
/**
* Removes all events for the specified entity and time interval
*
* @param tenantId
* @param entityId
* @param startTime
* @param endTime
*/
void removeEvents(UUID tenantId, UUID entityId, Long startTime, Long endTime); void removeEvents(UUID tenantId, UUID entityId, Long startTime, Long endTime);
/** void removeEvents(UUID tenantId, UUID entityId, Long startTime, Long endTime, EventType... types);
*
* Removes all events for the specified entity, event filter and time interval
*
* @param tenantId
* @param entityId
* @param eventFilter
* @param startTime
* @param endTime
*/
void removeEvents(UUID tenantId, UUID entityId, EventFilter eventFilter, Long startTime, Long endTime); void removeEvents(UUID tenantId, UUID entityId, EventFilter eventFilter, Long startTime, Long endTime);
} }

19
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -52,14 +52,15 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService; import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.exception.DataValidationException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -518,7 +519,13 @@ class BaseRelationService implements RelationService {
return Collections.emptyList(); return Collections.emptyList();
} }
List<EntityRelation> relations = relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations; List<EntityRelation> relations = relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations;
return relations.size() > limit ? relations.subList(0, limit) : relations; if (relations.size() > limit) {
List<EntityRelation> limitedRelations = new ArrayList<>(relations);
limitedRelations.sort(Comparator.comparing(r -> r.getFrom().getId()));
return limitedRelations.subList(0, limit);
} else {
return relations;
}
}, directExecutor()); }, directExecutor());
} }
return executor.submit(() -> { return executor.submit(() -> {
@ -545,7 +552,13 @@ class BaseRelationService implements RelationService {
case FROM -> findByFromAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); case FROM -> findByFromAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
case TO -> findByToAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); case TO -> findByToAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
}; };
return relations.size() > limit ? relations.subList(0, limit) : relations; if (relations.size() > limit) {
List<EntityRelation> limitedRelations = new ArrayList<>(relations);
limitedRelations.sort(Comparator.comparing(r -> r.getFrom().getId()));
return limitedRelations.subList(0, limit);
} else {
return relations;
}
} }
return relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit); return relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit);
} }

3
dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java

@ -31,9 +31,6 @@ import org.thingsboard.server.dao.model.sql.AssetInfoEntity;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* Created by Valerii Sosliuk on 5/21/2017.
*/
public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, ExportableEntityRepository<AssetEntity> { public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, ExportableEntityRepository<AssetEntity> {
@Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " +

3
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java

@ -86,9 +86,6 @@ public class JpaBaseEdgeEventDao extends JpaPartitionedAbstractDao<EdgeEventEnti
@Value("${sql.edge_events.partition_size:168}") @Value("${sql.edge_events.partition_size:168}")
private int partitionSizeInHours; private int partitionSizeInHours;
@Value("${sql.ttl.edge_events.edge_events_ttl:2628000}")
private long edgeEventsTtl;
private static final String TABLE_NAME = ModelConstants.EDGE_EVENT_TABLE_NAME; private static final String TABLE_NAME = ModelConstants.EDGE_EVENT_TABLE_NAME;
private TbSqlBlockingQueueWrapper<EdgeEventEntity, Void> queue; private TbSqlBlockingQueueWrapper<EdgeEventEntity, Void> queue;

3
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java

@ -30,9 +30,6 @@ import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* Created by Victor Basanets on 8/31/2017.
*/
public interface EntityViewRepository extends JpaRepository<EntityViewEntity, UUID>, ExportableEntityRepository<EntityViewEntity> { public interface EntityViewRepository extends JpaRepository<EntityViewEntity, UUID>, ExportableEntityRepository<EntityViewEntity> {
@Query("SELECT new org.thingsboard.server.dao.model.sql.EntityViewInfoEntity(e, c.title, c.additionalInfo) " + @Query("SELECT new org.thingsboard.server.dao.model.sql.EntityViewInfoEntity(e, c.title, c.additionalInfo) " +

3
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java

@ -45,9 +45,6 @@ import java.util.UUID;
import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto;
/**
* Created by Victor Basanets on 8/31/2017.
*/
@Component @Component
@Slf4j @Slf4j
@SqlDao @SqlDao

8
dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java

@ -173,8 +173,14 @@ public class JpaBaseEventDao implements EventDao {
@Override @Override
public void removeEvents(UUID tenantId, UUID entityId, Long startTime, Long endTime) { public void removeEvents(UUID tenantId, UUID entityId, Long startTime, Long endTime) {
removeEvents(tenantId, entityId, startTime, endTime, EventType.values());
}
@Override
public void removeEvents(UUID tenantId, UUID entityId, Long startTime, Long endTime, EventType... types) {
log.debug("[{}][{}] Remove events [{}-{}] ", tenantId, entityId, startTime, endTime); log.debug("[{}][{}] Remove events [{}-{}] ", tenantId, entityId, startTime, endTime);
for (EventType eventType : EventType.values()) { EventType[] eventTypes = (types == null || types.length == 0) ? EventType.values() : types;
for (EventType eventType : eventTypes) {
getEventRepository(eventType).removeEvents(tenantId, entityId, startTime, endTime); getEventRepository(eventType).removeEvents(tenantId, entityId, startTime, endTime);
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java

@ -67,7 +67,7 @@ public class JpaNotificationTargetDao extends JpaAbstractDao<NotificationTargetE
@Override @Override
public List<NotificationTarget> findByTenantIdAndUsersFilterType(TenantId tenantId, UsersFilterType filterType) { public List<NotificationTarget> findByTenantIdAndUsersFilterType(TenantId tenantId, UsersFilterType filterType) {
return DaoUtil.convertDataList(notificationTargetRepository.findByTenantIdAndSearchTextAndUsersFilterTypeIfPresent(tenantId.getId(), null, return DaoUtil.convertDataList(notificationTargetRepository.findByTenantIdAndUsersFilterType(tenantId.getId(),
List.of(filterType.name()), DaoUtil.toPageable(new PageLink(Integer.MAX_VALUE))).getContent()); List.of(filterType.name()), DaoUtil.toPageable(new PageLink(Integer.MAX_VALUE))).getContent());
} }

15
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java

@ -33,20 +33,27 @@ import java.util.UUID;
public interface NotificationTargetRepository extends JpaRepository<NotificationTargetEntity, UUID>, ExportableEntityRepository<NotificationTargetEntity> { public interface NotificationTargetRepository extends JpaRepository<NotificationTargetEntity, UUID>, ExportableEntityRepository<NotificationTargetEntity> {
@Query("SELECT t FROM NotificationTargetEntity t WHERE t.tenantId = :tenantId " + @Query("SELECT t FROM NotificationTargetEntity t WHERE t.tenantId = :tenantId " +
"AND (:searchText is NULL OR ilike(t.name, concat('%', :searchText, '%')) = true)") "AND (:searchText is NULL OR ilike(t.name, concat('%', :searchText, '%')) = true)")
Page<NotificationTargetEntity> findByTenantIdAndSearchText(@Param("tenantId") UUID tenantId, Page<NotificationTargetEntity> findByTenantIdAndSearchText(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query(value = "SELECT * FROM notification_target t WHERE t.tenant_id = :tenantId " + @Query(value = "SELECT * FROM notification_target t WHERE t.tenant_id = :tenantId " +
"AND (:searchText IS NULL OR t.name ILIKE concat('%', :searchText, '%')) " + "AND (:searchText IS NULL OR t.name ILIKE concat('%', :searchText, '%')) " +
"AND (cast(t.configuration as json) ->> 'type' <> 'PLATFORM_USERS' OR " + "AND (cast(t.configuration as json) ->> 'type' <> 'PLATFORM_USERS' OR " +
"cast(t.configuration as json) -> 'usersFilter' ->> 'type' IN :usersFilterTypes)", nativeQuery = true) "cast(t.configuration as json) -> 'usersFilter' ->> 'type' IN :usersFilterTypes)", nativeQuery = true)
Page<NotificationTargetEntity> findByTenantIdAndSearchTextAndUsersFilterTypeIfPresent(@Param("tenantId") UUID tenantId, Page<NotificationTargetEntity> findByTenantIdAndSearchTextAndUsersFilterTypeIfPresent(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText, @Param("searchText") String searchText,
@Param("usersFilterTypes") List<String> usersFilterTypes, @Param("usersFilterTypes") List<String> usersFilterTypes,
Pageable pageable); Pageable pageable);
@Query(value = "SELECT * FROM notification_target t WHERE t.tenant_id = :tenantId " +
"AND (cast(t.configuration as json) ->> 'type' = 'PLATFORM_USERS' AND " +
"cast(t.configuration as json) -> 'usersFilter' ->> 'type' IN :usersFilterTypes)", nativeQuery = true)
Page<NotificationTargetEntity> findByTenantIdAndUsersFilterType(@Param("tenantId") UUID tenantId,
@Param("usersFilterTypes") List<String> usersFilterTypes,
Pageable pageable);
List<NotificationTargetEntity> findByTenantIdAndIdIn(UUID tenantId, List<UUID> ids); List<NotificationTargetEntity> findByTenantIdAndIdIn(UUID tenantId, List<UUID> ids);
@Transactional @Transactional

10
dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java

@ -53,9 +53,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROU
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
/**
* Created by Valerii Sosliuk on 5/29/2017.
*/
@Slf4j @Slf4j
@Component @Component
@SqlDao @SqlDao
@ -260,9 +257,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
if (!CollectionUtils.isEmpty(relationTypeGroups)) { if (!CollectionUtils.isEmpty(relationTypeGroups)) {
sqlBuilder.append("AND relation_type_group IN (?"); sqlBuilder.append("AND relation_type_group IN (?");
for (int i = 1; i < relationTypeGroups.size(); i++) { sqlBuilder.append(", ?".repeat(Math.max(0, relationTypeGroups.size() - 1)));
sqlBuilder.append(", ?");
}
sqlBuilder.append(")"); sqlBuilder.append(")");
params.addAll(relationTypeGroups); params.addAll(relationTypeGroups);
} }
@ -289,8 +284,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
relation.setTypeGroup(RelationTypeGroup.valueOf((String) relationTypeGroup)); relation.setTypeGroup(RelationTypeGroup.valueOf((String) relationTypeGroup));
relation.setVersion((Long) version); relation.setVersion((Long) version);
return relation; return relation;
}) }).toList();
.collect(Collectors.toList());
} }
@Override @Override

3
dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java

@ -28,9 +28,6 @@ import org.thingsboard.server.dao.model.sql.TenantInfoEntity;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* Created by Valerii Sosliuk on 4/30/2017.
*/
public interface TenantRepository extends JpaRepository<TenantEntity, UUID> { public interface TenantRepository extends JpaRepository<TenantEntity, UUID> {
@Query("SELECT new org.thingsboard.server.dao.model.sql.TenantInfoEntity(t, p.name) " + @Query("SELECT new org.thingsboard.server.dao.model.sql.TenantInfoEntity(t, p.name) " +

3
dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/ApiUsageStateRepository.java

@ -29,9 +29,6 @@ import org.thingsboard.server.dao.model.sql.ApiUsageStateEntity;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* @author Valerii Sosliuk
*/
public interface ApiUsageStateRepository extends JpaRepository<ApiUsageStateEntity, UUID> { public interface ApiUsageStateRepository extends JpaRepository<ApiUsageStateEntity, UUID> {
@Query("SELECT ur FROM ApiUsageStateEntity ur WHERE ur.tenantId = :tenantId " + @Query("SELECT ur FROM ApiUsageStateEntity ur WHERE ur.tenantId = :tenantId " +

3
dao/src/main/java/org/thingsboard/server/dao/sql/usagerecord/JpaApiUsageStateDao.java

@ -34,9 +34,6 @@ import org.thingsboard.server.dao.util.SqlDao;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* @author Andrii Shvaika
*/
@Component @Component
@SqlDao @SqlDao
public class JpaApiUsageStateDao extends JpaAbstractDao<ApiUsageStateEntity, ApiUsageState> implements ApiUsageStateDao { public class JpaApiUsageStateDao extends JpaAbstractDao<ApiUsageStateEntity, ApiUsageState> implements ApiUsageStateDao {

3
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java

@ -53,9 +53,6 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
/**
* Created by Valerii Sosliuk on 4/29/2017.
*/
@Component @Component
@SqlDao @SqlDao
public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, WidgetTypeDetails> implements WidgetTypeDao, TenantEntityDao<WidgetTypeDetails> { public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, WidgetTypeDetails> implements WidgetTypeDao, TenantEntityDao<WidgetTypeDetails> {

3
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java

@ -42,9 +42,6 @@ import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
/**
* Created by Valerii Sosliuk on 4/23/2017.
*/
@Component @Component
@SqlDao @SqlDao
public class JpaWidgetsBundleDao extends JpaAbstractDao<WidgetsBundleEntity, WidgetsBundle> implements WidgetsBundleDao, TenantEntityDao<WidgetsBundle> { public class JpaWidgetsBundleDao extends JpaAbstractDao<WidgetsBundleEntity, WidgetsBundle> implements WidgetsBundleDao, TenantEntityDao<WidgetsBundle> {

3
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java

@ -28,9 +28,6 @@ import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* Created by Valerii Sosliuk on 4/23/2017.
*/
public interface WidgetsBundleRepository extends JpaRepository<WidgetsBundleEntity, UUID>, ExportableEntityRepository<WidgetsBundleEntity> { public interface WidgetsBundleRepository extends JpaRepository<WidgetsBundleEntity, UUID>, ExportableEntityRepository<WidgetsBundleEntity> {
WidgetsBundleEntity findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias); WidgetsBundleEntity findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias);

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java

@ -41,7 +41,7 @@ public class SqlPartitioningRepository {
@Autowired @Autowired
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
private static final String SELECT_PARTITIONS_STMT = "SELECT tablename from pg_tables WHERE schemaname = 'public' and tablename like concat(?, '_%')"; private static final String SELECT_PARTITIONS_STMT = "SELECT tablename from pg_tables WHERE schemaname = current_schema() and tablename like concat(?, '_%')";
private static final int PSQL_VERSION_14 = 140000; private static final int PSQL_VERSION_14 = 140000;
private volatile Integer currentServerVersion; private volatile Integer currentServerVersion;

8
dao/src/main/resources/sql/schema-ts-psql.sql

@ -72,7 +72,7 @@ BEGIN
IF partition_type = 'YEARS' THEN IF partition_type = 'YEARS' THEN
FOR partition IN SELECT tablename FOR partition IN SELECT tablename
FROM pg_tables FROM pg_tables
WHERE schemaname = 'public' WHERE schemaname = current_schema()
AND tablename like 'ts_kv_' || '%' AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest' AND tablename != 'ts_kv_latest'
AND tablename != 'key_dictionary' AND tablename != 'key_dictionary'
@ -93,7 +93,7 @@ BEGIN
ELSE ELSE
FOR partition IN SELECT tablename FOR partition IN SELECT tablename
FROM pg_tables FROM pg_tables
WHERE schemaname = 'public' WHERE schemaname = current_schema()
AND tablename like 'ts_kv_' || '%' AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest' AND tablename != 'ts_kv_latest'
AND tablename != 'key_dictionary' AND tablename != 'key_dictionary'
@ -135,7 +135,7 @@ BEGIN
ELSE ELSE
FOR partition IN SELECT tablename FOR partition IN SELECT tablename
FROM pg_tables FROM pg_tables
WHERE schemaname = 'public' WHERE schemaname = current_schema()
AND tablename like 'ts_kv_' || '%' AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest' AND tablename != 'ts_kv_latest'
AND tablename != 'key_dictionary' AND tablename != 'key_dictionary'
@ -205,7 +205,7 @@ BEGIN
IF partition IS NOT NULL THEN IF partition IS NOT NULL THEN
IF NOT EXISTS(SELECT IF NOT EXISTS(SELECT
FROM pg_tables FROM pg_tables
WHERE schemaname = 'public' WHERE schemaname = current_schema()
AND tablename = partition) THEN AND tablename = partition) THEN
partition := NULL; partition := NULL;
RAISE NOTICE 'Failed to found partition by ttl'; RAISE NOTICE 'Failed to found partition by ttl';

1
ui-ngx/src/app/core/auth/auth.models.ts

@ -35,6 +35,7 @@ export interface SysParamsState {
minAllowedAggregationIntervalInSecForCF: number; minAllowedAggregationIntervalInSecForCF: number;
minAllowedScheduledUpdateIntervalInSecForCF: number; minAllowedScheduledUpdateIntervalInSecForCF: number;
maxRelationLevelPerCfArgument: number; maxRelationLevelPerCfArgument: number;
maxRelatedEntitiesToReturnPerCfArgument: number;
ruleChainDebugPerTenantLimitsConfiguration?: string; ruleChainDebugPerTenantLimitsConfiguration?: string;
calculatedFieldDebugPerTenantLimitsConfiguration?: string; calculatedFieldDebugPerTenantLimitsConfiguration?: string;
intermediateAggregationIntervalInSecForCF: number; intermediateAggregationIntervalInSecForCF: number;

1
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -37,6 +37,7 @@ const emptyUserAuthState: AuthPayload = {
minAllowedAggregationIntervalInSecForCF: 0, minAllowedAggregationIntervalInSecForCF: 0,
minAllowedScheduledUpdateIntervalInSecForCF: 0, minAllowedScheduledUpdateIntervalInSecForCF: 0,
maxRelationLevelPerCfArgument: 0, maxRelationLevelPerCfArgument: 0,
maxRelatedEntitiesToReturnPerCfArgument: 0,
maxDataPointsPerRollingArg: 0, maxDataPointsPerRollingArg: 0,
maxDebugModeDurationMinutes: 0, maxDebugModeDurationMinutes: 0,
intermediateAggregationIntervalInSecForCF: 0, intermediateAggregationIntervalInSecForCF: 0,

9
ui-ngx/src/app/core/services/calculated-field-form.service.ts

@ -101,7 +101,12 @@ export class CalculatedFieldFormService {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe( .pipe(
switchMap(event => { switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null; let args = null;
if (event?.arguments) {
try {
args = JSON.parse(event.arguments);
} catch (e) {}
}
return testDialogFn(formValue, args, false, expression); return testDialogFn(formValue, args, false, expression);
}), }),
takeUntilDestroyed(destroyRef) takeUntilDestroyed(destroyRef)
@ -109,4 +114,4 @@ export class CalculatedFieldFormService {
} }
return testDialogFn(formValue, null, false, expression); return testDialogFn(formValue, null, false, expression);
} }
} }

19
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html

@ -79,6 +79,7 @@
[entityId]="entityId" [entityId]="entityId"
[tenantId]="tenantId" [tenantId]="tenantId"
[ownerId]="ownerId" [ownerId]="ownerId"
[isEditValue]="isEditValue"
[watchKeyChange]="true" [watchKeyChange]="true"
[entityName]="entityName"/> [entityName]="entityName"/>
</div> </div>
@ -97,14 +98,16 @@
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments" [testScript]="onTestScript.bind(this)" isClearCondition> <tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments" [testScript]="onTestScript.bind(this)" isClearCondition>
</tb-cf-alarm-rule> </tb-cf-alarm-rule>
</div> </div>
<button mat-icon-button @if (isEditValue) {
class="button-icon" <button mat-icon-button
type="button" class="button-icon"
(click)="removeClearAlarmRule()" type="button"
matTooltip="{{ 'action.remove' | translate }}" (click)="removeClearAlarmRule()"
matTooltipPosition="above"> matTooltip="{{ 'action.remove' | translate }}"
<mat-icon>delete</mat-icon> matTooltipPosition="above">
</button> <mat-icon>delete</mat-icon>
</button>
}
</div> </div>
<div *ngIf="!configFormGroup.get('clearRule').value"> <div *ngIf="!configFormGroup.get('clearRule').value">
<span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span> <span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span>

5
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts

@ -53,7 +53,7 @@ import { EntityService } from '@core/http/entity.service';
@Component({ @Component({
selector: 'tb-alarm-rules', selector: 'tb-alarm-rules',
templateUrl: './alarm-rules.component.html', templateUrl: './alarm-rules.component.html',
styleUrls: [] styleUrls: ['./alarm-rule-dialog.component.scss']
}) })
export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEntity> { export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEntity> {
@ -175,7 +175,8 @@ export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEn
expression: { expression: {
type: AlarmRuleExpressionType.SIMPLE type: AlarmRuleExpressionType.SIMPLE
} }
} },
alarmDetails: null
}; };
this.configFormGroup.patchValue({clearRule: clearAlarmRule}); this.configFormGroup.patchValue({clearRule: clearAlarmRule});
} }

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

@ -50,6 +50,7 @@
<tb-alarm-rule-filter-list <tb-alarm-rule-filter-list
[operation]="conditionFormGroup.get('expression.operation').value" [operation]="conditionFormGroup.get('expression.operation').value"
[arguments]="arguments" [arguments]="arguments"
[readonly]="readonly"
formControlName="filters"> formControlName="filters">
</tb-alarm-rule-filter-list> </tb-alarm-rule-filter-list>
</div> </div>
@ -75,7 +76,7 @@
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
class="tb-mat-32" class="tb-mat-32"
[disabled]="!argumentsList.length" [disabled]="!argumentsList.length || readonly"
(click)="onTestScript($event)"> (click)="onTestScript($event)">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> <mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button> </button>
@ -84,7 +85,7 @@
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
type="button" type="button"
(click)="onTestScript($event)" (click)="onTestScript($event)"
[disabled]="!argumentsList.length"> [disabled]="!argumentsList.length || readonly">
{{ 'calculated-fields.test-script-function' | translate }} {{ 'calculated-fields.test-script-function' | translate }}
</button> </button>
</div> </div>
@ -111,7 +112,7 @@
required> required>
</tb-alarm-rule-filter-text> </tb-alarm-rule-filter-text>
} @else { } @else {
<span class="tb-prompt text-base justify-center">{{ 'alarm-rule.no-filter-preview' | translate }}</span> <span class="tb-prompt justify-center text-base">{{ 'alarm-rule.no-filter-preview' | translate }}</span>
} }
</div> </div>
</ng-template> </ng-template>
@ -135,7 +136,7 @@
<span>{{ 'alarm-rule.condition-type-hint' | translate }}</span> <span>{{ 'alarm-rule.condition-type-hint' | translate }}</span>
</div> </div>
} }
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) { @if (conditionFormGroup.get('type').value === AlarmConditionType.DURATION) {
<div class="tb-form-panel stroked no-padding-bottom"> <div class="tb-form-panel stroked no-padding-bottom">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div> <div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
@ -167,7 +168,7 @@
</div> </div>
</div> </div>
</div> </div>
} @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) { } @else if (conditionFormGroup.get('type').value === AlarmConditionType.REPEATING) {
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div> <div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>

4
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts

@ -196,6 +196,10 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateSpecText(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); this.updateSpecText(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
if (this.readonly) {
this.conditionFormGroup.disable({emitEvent: false});
}
} }
updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) { updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) {

6
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html

@ -46,7 +46,7 @@
{{ dayOfWeekTranslationsArray[day] | translate }} {{ dayOfWeekTranslationsArray[day] | translate }}
</mat-chip-option> </mat-chip-option>
</mat-chip-listbox> </mat-chip-listbox>
<tb-error class="block mb-2" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required') <tb-error class="mb-2 block" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error> ? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
<div class="flex flex-row xs:flex-col gt-xs:gap-2"> <div class="flex flex-row xs:flex-col gt-xs:gap-2">
<div class="flex flex-row gap-2 gt-md:flex-1"> <div class="flex flex-row gap-2 gt-md:flex-1">
@ -63,7 +63,7 @@
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-1 items-center justify-center mb-[22px]"> <div class="flex flex-1 items-center justify-center" style="margin-bottom: 22px;">
<div class="text-center" [innerHTML]="getSchedulerRangeText(alarmScheduleForm.get('staticValue'))"></div> <div class="text-center" [innerHTML]="getSchedulerRangeText(alarmScheduleForm.get('staticValue'))"></div>
</div> </div>
</div> </div>
@ -90,7 +90,7 @@
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-1 items-center justify-center sm:max-w-[120px] text-center" <div class="flex flex-1 items-center justify-center text-center sm:max-w-[120px]"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))"> [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div> </div>
</div> </div>

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

@ -48,7 +48,7 @@
</div> </div>
} }
@if (!createAlarmRulesFormArray().controls.length) { @if (!createAlarmRulesFormArray().controls.length) {
<span translate class="flex items-center justify-center tb-prompt text-base required" [class.required]="!disabled"> <span translate class="tb-prompt required flex items-center justify-center text-base" [class.required]="!disabled">
alarm-rule.add-create-alarm-rule-prompt alarm-rule.add-create-alarm-rule-prompt
</span> </span>
} }

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

@ -52,10 +52,12 @@
cdkFocusInitial> cdkFocusInitial>
{{'action.cancel' | translate }} {{'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!readonly) {
type="submit" <button mat-raised-button color="primary"
[disabled]="complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty"> type="submit"
{{ (isAdd ? 'action.add' : 'action.update') | translate }} [disabled]="complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
</button> {{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
}
</div> </div>
</form> </form>

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

@ -36,6 +36,7 @@ export interface AlarmRuleComplexFilterPredicateDialogData {
valueType: EntityKeyValueType; valueType: EntityKeyValueType;
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
argumentInUse: string; argumentInUse: string;
readonly: boolean;
} }
@Component({ @Component({
@ -63,6 +64,8 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
arguments = this.data.arguments; arguments = this.data.arguments;
readonly = this.data.readonly;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData,
@ -73,6 +76,10 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
this.isAdd = this.data.isAdd; this.isAdd = this.data.isAdd;
this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false}); this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false});
if (this.readonly) {
this.complexFilterFormGroup.disable({emitEvent: false});
}
} }
cancel(): void { cancel(): void {

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

@ -46,7 +46,7 @@
</mat-icon> </mat-icon>
} }
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-1 tb-value-type" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker> <mat-form-field class="tb-value-type flex-1" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label translate>filter.value-type.value-type</mat-label> <mat-label translate>filter.value-type.value-type</mat-label>
<mat-select formControlName="valueType"> <mat-select formControlName="valueType">
<mat-select-trigger> <mat-select-trigger>
@ -93,10 +93,12 @@
cdkFocusInitial> cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!readonly) {
type="submit" <button mat-raised-button color="primary"
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty || !predicatesValid"> type="submit"
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }} [disabled]="filterFormGroup.invalid || !filterFormGroup.dirty || !predicatesValid">
</button> {{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
}
</div> </div>
</form> </form>

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

@ -39,6 +39,7 @@
isAdd: boolean; isAdd: boolean;
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
usedArguments: Array<string>; usedArguments: Array<string>;
readonly: boolean;
} }
@Component({ @Component({
@ -66,6 +67,8 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
arguments = this.data.arguments; arguments = this.data.arguments;
argumentsList: Array<string>; argumentsList: Array<string>;
readonly = this.data.readonly;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData,
@ -114,6 +117,10 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
} }
} }
}); });
if (this.readonly) {
this.filterFormGroup.disable({emitEvent: false});
}
} }
argumentInUse(argument: string): boolean { argumentInUse(argument: string): boolean {

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

@ -40,11 +40,11 @@
</div> </div>
<div class="flex max-w-92% flex-full flex-col"> <div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start"> <div class="flex flex-row items-center justify-start">
<div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div> <div class="filters-text flex-1">{{ filterControl.value?.argument }}</div>
<div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div> <div class="filters-text flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button" <button mat-icon-button color="primary" class="tb-budge-button"
type="button" type="button"
(click)="editFilter(index)" (click)="editFilter(index, readonly)"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}" matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon <mat-icon
@ -53,11 +53,12 @@
matBadgeSize="small" matBadgeSize="small"
matBadge="*" matBadge="*"
> >
edit {{ readonly ? 'visibility' : 'edit' }}
</mat-icon> </mat-icon>
</button> </button>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
type="button" type="button"
[disabled]="disabled || readonly"
(click)="removeFilter(index)" (click)="removeFilter(index)"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}" matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">

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

@ -71,6 +71,9 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
@Input() @Input()
operation: ComplexOperation = ComplexOperation.AND; operation: ComplexOperation = ComplexOperation.AND;
@Input()
readonly = false;
filterListFormGroup = this.fb.group({ filterListFormGroup = this.fb.group({
filters: this.fb.array([]) filters: this.fb.array([])
}); });
@ -143,17 +146,17 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
}); });
} }
public editFilter(index: number) { public editFilter(index: number, readonly = false) {
const filter: AlarmRuleFilter = const filter: AlarmRuleFilter =
(this.filterListFormGroup.get('filters') as FormArray).at(index).value; (this.filterListFormGroup.get('filters') as FormArray).at(index).value;
this.openFilterDialog(filter).subscribe(result => { this.openFilterDialog(filter, readonly).subscribe(result => {
if (result) { if (result) {
(this.filterListFormGroup.get('filters') as FormArray).at(index).patchValue(result); (this.filterListFormGroup.get('filters') as FormArray).at(index).patchValue(result);
} }
}); });
} }
private openFilterDialog(filter?: AlarmRuleFilter): Observable<AlarmRuleFilter> { private openFilterDialog(filter?: AlarmRuleFilter, readonly = false): Observable<AlarmRuleFilter> {
const isAdd = !filter; const isAdd = !filter;
if (isAdd) { if (isAdd) {
filter = { filter = {
@ -171,7 +174,8 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
filter: filter ? deepClone(filter) : null, filter: filter ? deepClone(filter) : null,
isAdd, isAdd,
arguments: this.arguments, arguments: this.arguments,
usedArguments: this.getUsedArguments usedArguments: this.getUsedArguments,
readonly
} }
}).afterClosed(); }).afterClosed();
} }

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

@ -31,7 +31,7 @@
<mat-divider style="padding-bottom: 5px;"></mat-divider> <mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list"> <div class="predicate-list">
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start relative" <div class="relative flex flex-row items-stretch justify-start"
[class.key-filter-list-divider]="$index"> [class.key-filter-list-divider]="$index">
@if (index) { @if (index) {
<div class="filters-operation-container"> <div class="filters-operation-container">

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

@ -80,6 +80,8 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
@Input() argumentInUse: string; @Input() argumentInUse: string;
@Input() readonly: boolean;
filterListFormGroup = this.fb.group({ filterListFormGroup = this.fb.group({
predicates: this.fb.array([]) predicates: this.fb.array([])
}); });
@ -197,7 +199,8 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
valueType: this.valueType, valueType: this.valueType,
isAdd: true, isAdd: true,
arguments: this.arguments, arguments: this.arguments,
argumentInUse: this.argumentInUse argumentInUse: this.argumentInUse,
readonly: this.readonly
} }
}).afterClosed().pipe( }).afterClosed().pipe(
map(result => result) map(result => result)

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

@ -17,7 +17,7 @@
--> -->
<div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueNoDataFormGroup"> <div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueNoDataFormGroup">
<div class="tb-form-row no-padding no-border column-xs"> <div class="tb-form-row no-padding no-border column-xs">
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="w-full max-w-30% flex-1 xs:max-w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}"> <mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option> <mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option>
<mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option> <mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option>
@ -39,7 +39,7 @@
} }
</mat-form-field> </mat-form-field>
} @else { } @else {
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="w-full flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}"> <mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) { @for (argument of argumentsList; track argument) {
<mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option> <mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option>

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

@ -17,7 +17,7 @@
--> -->
<div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueFormGroup"> <div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueFormGroup">
<div class="tb-form-row no-padding no-border column-xs"> <div class="tb-form-row no-padding no-border column-xs">
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="w-full max-w-30% flex-1 xs:max-w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}"> <mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option> <mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option>
<mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option> <mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option>
@ -52,7 +52,7 @@
} }
} }
} @else { } @else {
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="w-full flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}"> <mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) { @for (argument of argumentsList; track argument) {
<mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option> <mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option>

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

@ -19,7 +19,7 @@
<div class="tb-form-panel no-border no-padding-right flex-1" style="flex-direction: row"> <div class="tb-form-panel no-border no-padding-right flex-1" style="flex-direction: row">
@switch (type) { @switch (type) {
@case (filterPredicateType.STRING) { @case (filterPredicateType.STRING) {
<div class="tb-form-row no-padding no-border column-xs w-full max-w-30% xs:max-w-full flex-1/"> <div class="tb-form-row no-padding no-border column-xs flex-1/ w-full max-w-30% xs:max-w-full">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of stringOperations" [value]="operation"> <mat-option *ngFor="let operation of stringOperations" [value]="operation">
@ -64,7 +64,7 @@
(click)="openComplexFilterDialog()"> (click)="openComplexFilterDialog()">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-full text-start" translate>filter.complex-filter</span> <span class="w-full text-start" translate>filter.complex-filter</span>
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon> <mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ disabled ? 'visibility' : 'edit' }} </mat-icon>
</div> </div>
</button> </button>
</div> </div>

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

@ -203,6 +203,7 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
isAdd: false, isAdd: false,
arguments: this.arguments, arguments: this.arguments,
argumentInUse: this.argumentInUse, argumentInUse: this.argumentInUse,
readonly: this.disabled
} }
}).afterClosed().subscribe( }).afterClosed().subscribe(
(result) => { (result) => {

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

@ -101,6 +101,7 @@
[entityName]="entityName" [entityName]="entityName"
[ownerId]="ownerId" [ownerId]="ownerId"
[tenantId]="tenantId" [tenantId]="tenantId"
[isEditValue]="isEditValue"
></tb-geofencing-configuration> ></tb-geofencing-configuration>
} }
@case (CalculatedFieldType.PROPAGATION) { @case (CalculatedFieldType.PROPAGATION) {
@ -111,6 +112,7 @@
[tenantId]="tenantId" [tenantId]="tenantId"
[ownerId]="ownerId" [ownerId]="ownerId"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-propagation-configuration> ></tb-propagation-configuration>
} }
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
@ -120,6 +122,7 @@
[entityName]="entityName" [entityName]="entityName"
[tenantId]="tenantId" [tenantId]="tenantId"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-related-entities-aggregation-component> ></tb-related-entities-aggregation-component>
} }
@case (CalculatedFieldType.ENTITY_AGGREGATION) { @case (CalculatedFieldType.ENTITY_AGGREGATION) {
@ -128,6 +131,7 @@
[entityId]="entityId" [entityId]="entityId"
[entityName]="entityName" [entityName]="entityName"
[tenantId]="tenantId" [tenantId]="tenantId"
[isEditValue]="isEditValue"
></tb-entity-aggregation-component> ></tb-entity-aggregation-component>
} }
@default { @default {
@ -139,6 +143,7 @@
[tenantId]="tenantId" [tenantId]="tenantId"
[isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT" [isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-simple-configuration> ></tb-simple-configuration>
} }
} }

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

@ -93,7 +93,7 @@
<mat-header-cell *matHeaderCellDef class="w-20 min-w-20"/> <mat-header-cell *matHeaderCellDef class="w-20 min-w-20"/>
<mat-cell *matCellDef="let argument;"> <mat-cell *matCellDef="let argument;">
<div class="tb-form-table-row-cell-buttons min-w-20"> <div class="tb-form-table-row-cell-buttons min-w-20">
@if (disable) { @if (!isEditValue) {
<button type="button" <button type="button"
mat-icon-button mat-icon-button
#previewButton #previewButton

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts

@ -89,6 +89,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
@Input() ownerId: EntityId; @Input() ownerId: EntityId;
@Input() isScript: boolean; @Input() isScript: boolean;
@Input({transform: booleanAttribute}) disable = false; @Input({transform: booleanAttribute}) disable = false;
@Input({transform: booleanAttribute}) isEditValue = true;
@Input() watchKeyChange = false; @Input() watchKeyChange = false;
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;

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

@ -26,6 +26,7 @@
<tb-entity-aggregation-arguments-table formControlName="arguments" <tb-entity-aggregation-arguments-table formControlName="arguments"
[entityId]="entityId" [entityId]="entityId"
[tenantId]="tenantId" [tenantId]="tenantId"
[isEditValue]="isEditValue"
[entityName]="entityName"/> [entityName]="entityName"/>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input } from '@angular/core'; import { booleanAttribute, Component, forwardRef, Input } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -87,6 +87,8 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
@Input() testScript: (expression?: string) => Observable<string>; @Input() testScript: (expression?: string) => Observable<string>;
@Input({transform: booleanAttribute}) isEditValue = true;
readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF;
readonly intermediateAggregationIntervalInSecForCF = getCurrentAuthState(this.store).intermediateAggregationIntervalInSecForCF; readonly intermediateAggregationIntervalInSecForCF = getCurrentAuthState(this.store).intermediateAggregationIntervalInSecForCF;
readonly DayInSec = DAY / SECOND; readonly DayInSec = DAY / SECOND;

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

@ -96,7 +96,7 @@
<mat-header-cell *matHeaderCellDef class="w-20 min-w-20"/> <mat-header-cell *matHeaderCellDef class="w-20 min-w-20"/>
<mat-cell *matCellDef="let geofenceZone;"> <mat-cell *matCellDef="let geofenceZone;">
<div class="tb-form-table-row-cell-buttons min-w-20"> <div class="tb-form-table-row-cell-buttons min-w-20">
@if (disable) { @if (!isEditValue) {
<button type="button" <button type="button"
mat-icon-button mat-icon-button
#previewButton #previewButton

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

@ -16,6 +16,7 @@
import { import {
AfterViewInit, AfterViewInit,
booleanAttribute,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
@ -82,6 +83,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
@Input({required: true}) tenantId: string; @Input({required: true}) tenantId: string;
@Input({required: true}) entityName: string; @Input({required: true}) entityName: string;
@Input({required: true}) ownerId: EntityId; @Input({required: true}) ownerId: EntityId;
@Input({transform: booleanAttribute}) isEditValue = true;
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html

@ -44,6 +44,7 @@
[entityId]="entityId" [entityId]="entityId"
[tenantId]="tenantId" [tenantId]="tenantId"
[ownerId]="ownerId" [ownerId]="ownerId"
[isEditValue]="isEditValue"
[entityName]="entityName"/> [entityName]="entityName"/>
<div class="tb-form-row space-between flex-1 columns-xs" [class.!hidden]="!isRelatedEntity"> <div class="tb-form-row space-between flex-1 columns-xs" [class.!hidden]="!isRelatedEntity">
<mat-slide-toggle class="mat-slide" formControlName="scheduledUpdateEnabled"> <mat-slide-toggle class="mat-slide" formControlName="scheduledUpdateEnabled">

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { booleanAttribute, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -72,6 +72,8 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V
@Input({required: true}) @Input({required: true})
ownerId: EntityId; ownerId: EntityId;
@Input({transform: booleanAttribute}) isEditValue = true;
readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF;
readonly DataKeyType = DataKeyType; readonly DataKeyType = DataKeyType;

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html

@ -17,7 +17,7 @@
--> -->
<div [formGroup]="propagateConfiguration" class="tb-form-panel no-border no-padding"> <div [formGroup]="propagateConfiguration" class="tb-form-panel no-border no-padding">
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.propagation-path-related-entities' | translate }}"> <div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.propagation-path-related-entities' | translate: {max: maxRelatedEntitiesToReturnPerCfArgument} }}">
{{ 'calculated-fields.propagation-path-related-entities' | translate }} {{ 'calculated-fields.propagation-path-related-entities' | translate }}
</div> </div>
<div class="flex gap-3 xs:flex-col" formGroupName="relation"> <div class="flex gap-3 xs:flex-col" formGroupName="relation">
@ -56,6 +56,7 @@
[entityName]="entityName" [entityName]="entityName"
[ownerId]="ownerId" [ownerId]="ownerId"
[watchKeyChange]="true" [watchKeyChange]="true"
[isEditValue]="isEditValue"
[isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/> [isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/>
</div> </div>
<div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"> <div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value">

12
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input } from '@angular/core'; import { booleanAttribute, Component, forwardRef, Input } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -43,6 +43,9 @@ import { map } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@app/shared/models/rule-node.models'; import { ScriptLanguage } from '@app/shared/models/rule-node.models';
import { EntitySearchDirection } from '@shared/models/relation.models'; import { EntitySearchDirection } from '@shared/models/relation.models';
import {Store} from "@ngrx/store";
import {AppState} from "@core/core.state";
import {getCurrentAuthState} from "@core/auth/auth.selectors";
@Component({ @Component({
selector: 'tb-propagation-configuration', selector: 'tb-propagation-configuration',
@ -77,6 +80,10 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
@Input({required: true}) @Input({required: true})
testScript: () => Observable<string>; testScript: () => Observable<string>;
@Input({transform: booleanAttribute}) isEditValue = true;
readonly maxRelatedEntitiesToReturnPerCfArgument = getCurrentAuthState(this.store).maxRelatedEntitiesToReturnPerCfArgument;
propagateConfiguration = this.fb.group({ propagateConfiguration = this.fb.group({
arguments: this.fb.control({}, notEmptyObjectValidator()), arguments: this.fb.control({}, notEmptyObjectValidator()),
applyExpressionToResolvedArguments: [false], applyExpressionToResolvedArguments: [false],
@ -110,7 +117,8 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { }; private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { };
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder,
private store: Store<AppState>) {
this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe( this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
).subscribe(() => { ).subscribe(() => {

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

@ -17,7 +17,7 @@
--> -->
<div [formGroup]="relatedAggregationConfiguration" class="tb-form-panel no-border no-padding"> <div [formGroup]="relatedAggregationConfiguration" class="tb-form-panel no-border no-padding">
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.aggregation-path-related-entities' | translate }}"> <div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.aggregation-path-related-entities' | translate: {max: maxRelatedEntitiesToReturnPerCfArgument} }}">
{{ 'calculated-fields.aggregation-path-related-entities' | translate }} {{ 'calculated-fields.aggregation-path-related-entities' | translate }}
</div> </div>
<div class="flex gap-3 xs:flex-col" formGroupName="relation"> <div class="flex gap-3 xs:flex-col" formGroupName="relation">
@ -48,6 +48,7 @@
[entityId]="entityId" [entityId]="entityId"
[tenantId]="tenantId" [tenantId]="tenantId"
[entityName]="entityName" [entityName]="entityName"
[isEditValue]="isEditValue"
[relation]="relatedAggregationConfiguration.get('relation').value"/> [relation]="relatedAggregationConfiguration.get('relation').value"/>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input } from '@angular/core'; import { booleanAttribute, Component, forwardRef, Input } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -76,12 +76,15 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
@Input({required: true}) @Input({required: true})
testScript: (expression?: string) => Observable<string>; testScript: (expression?: string) => Observable<string>;
@Input({transform: booleanAttribute}) isEditValue = true;
readonly ScriptLanguage = ScriptLanguage; readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType; readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType; readonly OutputType = OutputType;
readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>; readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>;
readonly PropagationDirectionTranslations = PropagationDirectionTranslations; readonly PropagationDirectionTranslations = PropagationDirectionTranslations;
readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF;
readonly maxRelatedEntitiesToReturnPerCfArgument = getCurrentAuthState(this.store).maxRelatedEntitiesToReturnPerCfArgument;
relatedAggregationConfiguration = this.fb.group({ relatedAggregationConfiguration = this.fb.group({
relation: this.fb.group({ relation: this.fb.group({

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html

@ -26,6 +26,7 @@
[tenantId]="tenantId" [tenantId]="tenantId"
[ownerId]="ownerId" [ownerId]="ownerId"
[entityName]="entityName" [entityName]="entityName"
[isEditValue]="isEditValue"
[watchKeyChange]="true" [watchKeyChange]="true"
[isScript]="isScript" /> [isScript]="isScript" />
</div> </div>

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { booleanAttribute, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -81,6 +81,8 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
@Input({required: true}) @Input({required: true})
testScript: () => Observable<string>; testScript: () => Observable<string>;
@Input({transform: booleanAttribute}) isEditValue = true;
simpleConfiguration = this.fb.group({ simpleConfiguration = this.fb.group({
arguments: this.fb.control({}), arguments: this.fb.control({}),
expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],

2
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -661,7 +661,7 @@ export class TbTimeSeriesChart {
} }
} }
} else if (limit.type === ValueSourceType.constant) { } else if (limit.type === ValueSourceType.constant) {
const value = unitConvertor ? unitConvertor(limit.value) : limit.value; const value = unitConvertor && isDefinedAndNotNull(limit.value) ? unitConvertor(limit.value) : limit.value;
if (limitType === 'min') { if (limitType === 'min') {
yAxis.option.min = value; yAxis.option.min = value;
} else { } else {

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

@ -22,13 +22,10 @@ import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models'; import { MenuId } from '@core/services/menu.models';
import { RouterTabsComponent } from "@home/components/router-tabs.component"; import { RouterTabsComponent } from "@home/components/router-tabs.component";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component"; import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component";
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { CalculatedFieldsTableConfigResolver } from '@home/pages/calculated-fields/calculated-fields-routing.module';
import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';

2
ui-ngx/src/app/shared/components/time-unit-input.component.html

@ -22,7 +22,7 @@
@if (labelText && !inlineField) { @if (labelText && !inlineField) {
<mat-label>{{ labelText }}</mat-label> <mat-label>{{ labelText }}</mat-label>
} }
<input type="number" min="0" step="1" matInput formControlName="time" placeholder="{{ 'widget-config.set' | translate }}"> <input type="number" [min]="minValueValidator" step="1" matInput formControlName="time" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix> <div matSuffix>
<ng-content select="[matSuffix]"></ng-content> <ng-content select="[matSuffix]"></ng-content>
</div> </div>

15
ui-ngx/src/app/shared/components/time-unit-input.component.ts

@ -120,6 +120,8 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
[TimeUnit.SECONDS, SECOND/SECOND], [TimeUnit.SECONDS, SECOND/SECOND],
]); ]);
minValueValidator = 0;
private modelValue: number; private modelValue: number;
private propagateChange: (value: any) => void = () => {}; private propagateChange: (value: any) => void = () => {};
@ -144,8 +146,9 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
); );
} }
if (isDefinedAndNotNull(this.minTime)) { if (isDefinedAndNotNull(this.minTime)) {
this.minValueValidator = this.minValue;
validators.push((control: AbstractControl) => validators.push((control: AbstractControl) =>
Validators.min(Math.ceil(this.minTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)))(control) Validators.min(this.minValueValidator)(control)
); );
} }
@ -160,6 +163,9 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
this.timeInputForm.get('timeUnit').valueChanges.pipe( this.timeInputForm.get('timeUnit').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => { ).subscribe(() => {
if (isDefinedAndNotNull(this.minTime)) {
this.minValueValidator = this.minValue;
}
this.timeInputForm.get('time').updateValueAndValidity({onlySelf: true}); this.timeInputForm.get('time').updateValueAndValidity({onlySelf: true});
this.timeInputForm.get('time').markAsTouched({onlySelf: true}); this.timeInputForm.get('time').markAsTouched({onlySelf: true});
}); });
@ -171,6 +177,10 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
}); });
} }
get minValue(): number {
return Math.ceil(this.minTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value));
}
get hasError(): string { get hasError(): string {
if (this.timeInputForm.get('time').hasError('required') && this.requiredText) { if (this.timeInputForm.get('time').hasError('required') && this.requiredText) {
return this.requiredText; return this.requiredText;
@ -194,6 +204,9 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
this.timeInputForm.get('time').updateValueAndValidity({emitEvent: false}); this.timeInputForm.get('time').updateValueAndValidity({emitEvent: false});
} }
} }
if (propName === 'minTime') {
this.minValueValidator = isDefinedAndNotNull(this.minTime) ? this.minValue : 0;
}
} }
} }
} }

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

@ -1379,9 +1379,9 @@
"zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.", "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.",
"zone-group-refresh-interval-required": "Zone groups refresh interval is required.", "zone-group-refresh-interval-required": "Zone groups refresh interval is required.",
"zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} seconds.", "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} seconds.",
"propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type.", "propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type. Only relations between device, asset, customer, and tenant entities are supported. Maximum entities resolved by relation path is {{ max }}.",
"data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.", "data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.",
"aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities, based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported.", "aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities, based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported. Maximum entities resolved by relation path is {{ max }}.",
"arguments-aggregation": "Defines the input arguments used for filtering and aggregation.", "arguments-aggregation": "Defines the input arguments used for filtering and aggregation.",
"setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.", "setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.",
"metrics": "Defines metrics aggregated based on configured arguments.", "metrics": "Defines metrics aggregated based on configured arguments.",

2
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -11,7 +11,7 @@
"permission-denied-text": "이 작업을 수행할 권한이 없습니다!" "permission-denied-text": "이 작업을 수행할 권한이 없습니다!"
}, },
"action": { "action": {
"activate": "활설화", "activate": "활성화",
"suspend": "비활성화", "suspend": "비활성화",
"save": "저장", "save": "저장",
"saveAs": "다른 이름으로 저장", "saveAs": "다른 이름으로 저장",

Loading…
Cancel
Save