Browse Source

Merge pull request #14815 from thingsboard/rc

Rc
pull/14973/head
Viacheslav Klimov 7 months ago
committed by GitHub
parent
commit
8ca6cc01a4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  3. 4
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  4. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  5. 29
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java
  6. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java
  7. 18
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java
  8. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java
  9. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java
  10. 12
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  11. 4
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java
  12. 14
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java
  13. 15
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java
  14. 15
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  15. 12
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  16. 8
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  17. 7
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  18. 6
      common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java
  19. 42
      common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java
  20. 14
      msa/edqs/docker/start-tb-edqs.sh
  21. 11
      ui-ngx/src/app/core/utils.ts
  22. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  23. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
  24. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
  25. 1
      ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html
  26. 2
      ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts
  27. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts
  28. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  29. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  30. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  31. 1
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts
  32. 4
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html
  33. 2
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts
  34. 14
      ui-ngx/src/app/shared/components/country-autocomplete.component.html
  35. 29
      ui-ngx/src/app/shared/components/country-autocomplete.component.scss
  36. 62
      ui-ngx/src/app/shared/components/country-autocomplete.component.ts
  37. 2
      ui-ngx/src/app/shared/components/time-unit-input.component.ts
  38. 4
      ui-ngx/src/app/shared/models/tenant.model.ts
  39. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
application/src/main/data/upgrade/basic/schema_update.sql

@ -21,8 +21,8 @@ SET profile_data = jsonb_set(
profile_data, profile_data,
'{configuration}', '{configuration}',
jsonb_build_object( jsonb_build_object(
'minAllowedScheduledUpdateIntervalInSecForCF', 60, 'minAllowedScheduledUpdateIntervalInSecForCF', 10,
'maxRelationLevelPerCfArgument', 10, 'maxRelationLevelPerCfArgument', 2,
'maxRelatedEntitiesToReturnPerCfArgument', 100, 'maxRelatedEntitiesToReturnPerCfArgument', 100,
'minAllowedDeduplicationIntervalInSecForCF', 10, 'minAllowedDeduplicationIntervalInSecForCF', 10,
'minAllowedAggregationIntervalInSecForCF', 60, 'minAllowedAggregationIntervalInSecForCF', 60,

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

@ -384,6 +384,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
EntityId mainId, EntityId mainId,
MultipleTbCallback parentCallback, MultipleTbCallback parentCallback,
TriConsumer<EntityId, CalculatedFieldCtx, TbCallback> relationAction) { TriConsumer<EntityId, CalculatedFieldCtx, TbCallback> relationAction) {
if (isMyPartition(mainId, parentCallback)) {
return;
}
List<CalculatedFieldCtx> cfsByEntityIdAndProfile = getCalculatedFieldsByEntityIdAndProfile(mainId); List<CalculatedFieldCtx> cfsByEntityIdAndProfile = getCalculatedFieldsByEntityIdAndProfile(mainId);
if (cfsByEntityIdAndProfile.isEmpty()) { if (cfsByEntityIdAndProfile.isEmpty()) {
parentCallback.onSuccess(); parentCallback.onSuccess();

4
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -161,8 +161,8 @@ public class TenantProfileController extends BaseController {
" \"warnThreshold\": 0,\n" + " \"warnThreshold\": 0,\n" +
" \"maxCalculatedFieldsPerEntity\": 5,\n" + " \"maxCalculatedFieldsPerEntity\": 5,\n" +
" \"maxArgumentsPerCF\": 10,\n" + " \"maxArgumentsPerCF\": 10,\n" +
" \"minAllowedScheduledUpdateIntervalInSecForCF\": 60,\n" + " \"minAllowedScheduledUpdateIntervalInSecForCF\": 10,\n" +
" \"maxRelationLevelPerCfArgument\": 10,\n" + " \"maxRelationLevelPerCfArgument\": 2,\n" +
" \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" + " \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" +
" \"maxDataPointsPerRollingArg\": 1000,\n" + " \"maxDataPointsPerRollingArg\": 1000,\n" +
" \"maxStateSizeInKBytes\": 32,\n" + " \"maxStateSizeInKBytes\": 32,\n" +

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

@ -129,6 +129,8 @@ public class CalculatedFieldCtx implements Closeable {
private long cfCheckReevaluationIntervalMillis; private long cfCheckReevaluationIntervalMillis;
private long alarmReevaluationIntervalMillis; private long alarmReevaluationIntervalMillis;
private long maxRelatedEntitiesPerCfArgument; private long maxRelatedEntitiesPerCfArgument;
private long minScheduledUpdateIntervalMillis;
private long minDeduplicationIntervalMillis;
private Argument propagationArgument; private Argument propagationArgument;
private boolean applyExpressionForResolvedArguments; private boolean applyExpressionForResolvedArguments;
@ -312,6 +314,8 @@ public class CalculatedFieldCtx implements Closeable {
this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getCfReevaluationCheckInterval()); this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getCfReevaluationCheckInterval());
this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getAlarmsReevaluationInterval()); this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getAlarmsReevaluationInterval());
this.maxRelatedEntitiesPerCfArgument = config.getMaxRelatedEntitiesToReturnPerCfArgument(); this.maxRelatedEntitiesPerCfArgument = config.getMaxRelatedEntitiesToReturnPerCfArgument();
this.minScheduledUpdateIntervalMillis = TimeUnit.SECONDS.toMillis(config.getMinAllowedScheduledUpdateIntervalInSecForCF());
this.minDeduplicationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getMinAllowedDeduplicationIntervalInSecForCF());
}); });
} }
@ -763,9 +767,7 @@ public class CalculatedFieldCtx implements Closeable {
} }
public boolean hasRelatedEntities() { public boolean hasRelatedEntities() {
return CalculatedFieldType.GEOFENCING == cfType return cfHasRelationPathQuerySource;
|| CalculatedFieldType.PROPAGATION == cfType
|| CalculatedFieldType.RELATED_ENTITIES_AGGREGATION == cfType;
} }
public boolean shouldFetchRelatedEntities(CalculatedFieldState state) { public boolean shouldFetchRelatedEntities(CalculatedFieldState state) {
@ -781,7 +783,7 @@ public class CalculatedFieldCtx implements Closeable {
if (scheduledRefreshSupported.getLastScheduledRefreshTs() == DEFAULT_LAST_UPDATE_TS) { if (scheduledRefreshSupported.getLastScheduledRefreshTs() == DEFAULT_LAST_UPDATE_TS) {
return true; return true;
} }
return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - Math.max(scheduledUpdateIntervalMillis, minScheduledUpdateIntervalMillis);
} }
@Override @Override

29
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf.ctx.state;
public interface HasEntityLimit {
default void checkEntityLimit(int currentEntitiesCount, CalculatedFieldCtx ctx) {
if (currentEntitiesCount >= ctx.getMaxRelatedEntitiesPerCfArgument()) {
throw new IllegalArgumentException(
"Exceeded the maximum allowed related entities per argument '"
+ ctx.getMaxRelatedEntitiesPerCfArgument() + "'. Increase the limit in the tenant profile configuration."
);
}
}
}

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

@ -183,7 +183,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
} }
public void scheduleReevaluation() { public void scheduleReevaluation() {
ScheduledFuture<?> future = ctx.scheduleReevaluation(deduplicationIntervalMs, actorCtx); ScheduledFuture<?> future = ctx.scheduleReevaluation(getEnforcedDeduplicationIntervalMillis(), actorCtx);
if (future != null) { if (future != null) {
reevaluationFuture = future; reevaluationFuture = future;
} }
@ -209,11 +209,15 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
} }
private boolean shouldRecalculate() { private boolean shouldRecalculate() {
boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - deduplicationIntervalMs; boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - getEnforcedDeduplicationIntervalMillis();
boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs; boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs;
return intervalPassed && argsUpdatedDuringInterval; return intervalPassed && argsUpdatedDuringInterval;
} }
private long getEnforcedDeduplicationIntervalMillis() {
return Math.max(deduplicationIntervalMs, ctx.getMinDeduplicationIntervalMillis());
}
private Map<EntityId, Map<String, ArgumentEntry>> prepareInputs() { private Map<EntityId, Map<String, ArgumentEntry>> prepareInputs() {
Map<EntityId, Map<String, ArgumentEntry>> inputs = new HashMap<>(); Map<EntityId, Map<String, ArgumentEntry>> inputs = new HashMap<>();
for (Map.Entry<String, ArgumentEntry> argEntry : arguments.entrySet()) { for (Map.Entry<String, ArgumentEntry> argEntry : arguments.entrySet()) {

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

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.HasEntityLimit;
import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import org.thingsboard.server.service.cf.ctx.state.HasLatestTs;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
@ -34,7 +35,7 @@ import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldSta
@Data @Data
@AllArgsConstructor @AllArgsConstructor
public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs { public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs, HasEntityLimit {
private final Map<EntityId, ArgumentEntry> entityInputs; private final Map<EntityId, ArgumentEntry> entityInputs;
@ -66,18 +67,18 @@ 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); checkEntityLimit(entityInputs.size(), ctx);
entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs);
} else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { } else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
if (entry.isForceResetPrevious()) { if (entry.isForceResetPrevious()) {
checkRelatedEntitiesNumber(ctx); checkEntityLimit(entityInputs.size(), ctx);
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry);
} else { } else {
ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId());
if (argumentEntry != null) { if (argumentEntry != null) {
argumentEntry.updateEntry(singleValueArgumentEntry, ctx); argumentEntry.updateEntry(singleValueArgumentEntry, ctx);
} else { } else {
checkRelatedEntitiesNumber(ctx); checkEntityLimit(entityInputs.size(), ctx);
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry);
} }
} }
@ -87,15 +88,6 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs
return true; 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
public boolean isEmpty() { public boolean isEmpty() {
return entityInputs.isEmpty(); return entityInputs.isEmpty();

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java

@ -51,9 +51,10 @@ public class GeofencingZoneState {
this.ts = attributeKvEntry.getLastUpdateTs(); this.ts = attributeKvEntry.getLastUpdateTs();
this.version = attributeKvEntry.getVersion(); this.version = attributeKvEntry.getVersion();
if (entry.getValueAsString() == null) { if (entry.getValueAsString() == null) {
throw new IllegalArgumentException("Perimeter attribute key '" + entry.getKey() + "' not found for Zone with id: " + zoneId); throw new IllegalArgumentException("Perimeter attribute '" + entry.getKey() + "' not found for Zone with id: " + zoneId);
} }
this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class); this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class,
"Invalid perimeter definition format for Zone with id: " + zoneId + ". Failed to parse attribute '" + entry.getKey() + "'");
} }
public GeofencingZoneState(GeofencingZoneProto proto) { public GeofencingZoneState(GeofencingZoneProto proto) {

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

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.HasEntityLimit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -30,7 +31,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
@Data @Data
public class PropagationArgumentEntry implements ArgumentEntry { public class PropagationArgumentEntry implements ArgumentEntry, HasEntityLimit {
private Set<EntityId> entityIds; private Set<EntityId> entityIds;
private transient List<EntityId> added; private transient List<EntityId> added;
@ -93,12 +94,7 @@ public class PropagationArgumentEntry implements ArgumentEntry {
private boolean checkAdded(Collection<EntityId> updatedIds, CalculatedFieldCtx ctx) { private boolean checkAdded(Collection<EntityId> updatedIds, CalculatedFieldCtx ctx) {
for (EntityId id : updatedIds) { for (EntityId id : updatedIds) {
if (entityIds.size() >= ctx.getMaxRelatedEntitiesPerCfArgument()) { checkEntityLimit(entityIds.size(), ctx);
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<>();

12
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java

@ -25,6 +25,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
@ -44,8 +45,9 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
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.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
@ -54,6 +56,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -92,7 +95,9 @@ public class GeofencingCalculatedFieldStateTest {
private CalculatedFieldCtx ctx; private CalculatedFieldCtx ctx;
@Mock @Mock
private ApiLimitService apiLimitService; private TenantProfile tenantProfile;
@Mock
private TbTenantProfileCache tenantProfileCache;
@Mock @Mock
private RelationService relationService; private RelationService relationService;
@InjectMocks @InjectMocks
@ -100,7 +105,8 @@ public class GeofencingCalculatedFieldStateTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile);
when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration()));
ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext);
ctx.init(); ctx.init();
state = new GeofencingCalculatedFieldState(ctx.getEntityId()); state = new GeofencingCalculatedFieldState(ctx.getEntityId());

4
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java

@ -177,7 +177,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new StringDataEntry("zone", "someString"), 363L, 155L); BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new StringDataEntry("zone", "someString"), 363L, 155L);
assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry)))
.isExactlyInstanceOf(IllegalArgumentException.class) .isExactlyInstanceOf(IllegalArgumentException.class)
.hasMessage("The given string value cannot be transformed to Json object: someString"); .hasMessage("Invalid perimeter definition format for Zone with id: " + ZONE_1_ID + ". Failed to parse attribute 'zone'");
} }
@Test @Test
@ -185,7 +185,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L); BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L);
assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry)))
.isExactlyInstanceOf(IllegalArgumentException.class) .isExactlyInstanceOf(IllegalArgumentException.class)
.hasMessage("The given string value cannot be transformed to Json object: \"{}\""); .hasMessage("Invalid perimeter definition format for Zone with id: " + ZONE_1_ID + ". Failed to parse attribute 'zone'");
} }
} }

14
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java

@ -29,6 +29,7 @@ import org.thingsboard.script.api.tbel.DefaultTbelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
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.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
@ -45,8 +46,9 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
@ -106,7 +108,10 @@ public class PropagationCalculatedFieldStateTest {
private TbelInvokeService tbelInvokeService; private TbelInvokeService tbelInvokeService;
@MockitoBean @MockitoBean
private ApiLimitService apiLimitService; private TenantProfile tenantProfile;
@MockitoBean
private TbTenantProfileCache tenantProfileCache;
@MockitoBean @MockitoBean
private ActorSystemContext actorSystemContext; private ActorSystemContext actorSystemContext;
@ -117,9 +122,10 @@ public class PropagationCalculatedFieldStateTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService);
when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService);
when(actorSystemContext.getCalculatedFieldProcessingService()).thenReturn(cfProcessingService); when(actorSystemContext.getCalculatedFieldProcessingService()).thenReturn(cfProcessingService);
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); when(actorSystemContext.getTenantProfileCache()).thenReturn(tenantProfileCache);
when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile);
when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration()));
} }
void initCtxAndState(boolean applyExpressionToResolvedArguments) { void initCtxAndState(boolean applyExpressionToResolvedArguments) {

15
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java

@ -25,6 +25,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
@ -46,14 +47,16 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -79,7 +82,10 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest {
private TbelInvokeService tbelInvokeService; private TbelInvokeService tbelInvokeService;
@MockitoBean @MockitoBean
private ApiLimitService apiLimitService; private TenantProfile tenantProfile;
@MockitoBean
private TbTenantProfileCache tenantProfileCache;
@MockitoBean @MockitoBean
private ActorSystemContext actorSystemContext; private ActorSystemContext actorSystemContext;
@ -87,8 +93,9 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService);
when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService); when(actorSystemContext.getTenantProfileCache()).thenReturn(tenantProfileCache);
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile);
when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration()));
initCtxAndState(); initCtxAndState();
} }

15
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java

@ -27,6 +27,7 @@ import org.thingsboard.script.api.tbel.DefaultTbelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
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.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
@ -41,13 +42,15 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -75,15 +78,19 @@ public class ScriptCalculatedFieldStateTest {
private TbelInvokeService tbelInvokeService; private TbelInvokeService tbelInvokeService;
@MockitoBean @MockitoBean
private ApiLimitService apiLimitService; private TenantProfile tenantProfile;
@MockitoBean
private TbTenantProfileCache tenantProfileCache;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class); ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class);
when(systemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); when(systemContext.getTbelInvokeService()).thenReturn(tbelInvokeService);
when(systemContext.getApiLimitService()).thenReturn(apiLimitService); when(systemContext.getTenantProfileCache()).thenReturn(tenantProfileCache);
when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile);
when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration()));
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L);
ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext);
ctx.init(); ctx.init();
state = new ScriptCalculatedFieldState(ctx.getEntityId()); state = new ScriptCalculatedFieldState(ctx.getEntityId());

12
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -24,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
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.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
@ -40,12 +41,14 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -69,13 +72,16 @@ public class SimpleCalculatedFieldStateTest {
private CalculatedFieldCtx ctx; private CalculatedFieldCtx ctx;
@Mock @Mock
private ApiLimitService apiLimitService; private TenantProfile tenantProfile;
@Mock
private TbTenantProfileCache tenantProfileCache;
@InjectMocks @InjectMocks
private ActorSystemContext systemContext; private ActorSystemContext systemContext;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile);
when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration()));
ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext);
ctx.init(); ctx.init();
state = new SimpleCalculatedFieldState(ctx.getEntityId()); state = new SimpleCalculatedFieldState(ctx.getEntityId());

8
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -172,12 +172,12 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxCalculatedFieldsPerEntity = 5; private long maxCalculatedFieldsPerEntity = 5;
@Schema(example = "10") @Schema(example = "10")
private long maxArgumentsPerCF = 10; private long maxArgumentsPerCF = 10;
@Schema(example = "60")
private int minAllowedScheduledUpdateIntervalInSecForCF = 60;
@Builder.Default
@Schema(example = "10") @Schema(example = "10")
private int minAllowedScheduledUpdateIntervalInSecForCF = 10;
@Builder.Default
@Schema(example = "2")
@Positive @Positive
private int maxRelationLevelPerCfArgument = 10; private int maxRelationLevelPerCfArgument = 2;
@Builder.Default @Builder.Default
@Schema(example = "100") @Schema(example = "100")
@Positive @Positive

7
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -115,10 +115,15 @@ public class JacksonUtil {
@Contract("null, _ -> null") // so that IDE doesn't show NPE warning when input is not null @Contract("null, _ -> null") // so that IDE doesn't show NPE warning when input is not null
public static <T> T fromString(String string, Class<T> clazz) { public static <T> T fromString(String string, Class<T> clazz) {
return fromString(string, clazz, "The given string value cannot be transformed to Json object: " + string);
}
@Contract("null, _, _ -> null") // so that IDE doesn't show NPE warning when input is not null
public static <T> T fromString(String string, Class<T> clazz, String errorMsg) {
try { try {
return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException("The given string value cannot be transformed to Json object: " + string, e); throw new IllegalArgumentException(errorMsg, e);
} }
} }

6
common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -32,6 +33,9 @@ public class PerimeterDefinitionDeserializer extends JsonDeserializer<PerimeterD
JsonNode node = codec.readTree(p); JsonNode node = codec.readTree(p);
if (node.isObject()) { if (node.isObject()) {
if (!node.has("latitude") || !node.has("longitude") || !node.has("radius")) {
throw JsonMappingException.from(p, "CirclePerimeterDefinition missing required fields. Received: " + node);
}
double latitude = node.get("latitude").asDouble(); double latitude = node.get("latitude").asDouble();
double longitude = node.get("longitude").asDouble(); double longitude = node.get("longitude").asDouble();
double radius = node.get("radius").asDouble(); double radius = node.get("radius").asDouble();
@ -42,7 +46,7 @@ public class PerimeterDefinitionDeserializer extends JsonDeserializer<PerimeterD
String polygonStrDefinition = mapper.writeValueAsString(node); String polygonStrDefinition = mapper.writeValueAsString(node);
return new PolygonPerimeterDefinition(polygonStrDefinition); return new PolygonPerimeterDefinition(polygonStrDefinition);
} }
throw new IOException("Failed to deserialize PerimeterDefinition from node: " + node); throw JsonMappingException.from(p, "Unknown JSON format for PerimeterDefinition. Expected OBJECT (Circle) or ARRAY (Polygon), but found: " + node.getNodeType());
} }
} }

42
common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java

@ -15,10 +15,12 @@
*/ */
package org.thingsboard.common.util.geo; package org.thingsboard.common.util.geo;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class PerimeterDefinitionDeserializerTest { public class PerimeterDefinitionDeserializerTest {
@ -47,4 +49,44 @@ public class PerimeterDefinitionDeserializerTest {
PolygonPerimeterDefinition poly = (PolygonPerimeterDefinition) def; PolygonPerimeterDefinition poly = (PolygonPerimeterDefinition) def;
assertThat(poly.getPolygonDefinition()).isEqualTo(json); assertThat(poly.getPolygonDefinition()).isEqualTo(json);
} }
@Test
void shouldThrowWhenCircleFieldIsMissing() {
// Missing "radius"
String badJson = """
{
"latitude": 48.8566,
"longitude": 2.3522
}
""";
String customError = "Custom error context";
assertThatThrownBy(() -> JacksonUtil.fromString(badJson, PerimeterDefinition.class, customError))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(customError)
.hasCauseInstanceOf(JsonMappingException.class)
.rootCause()
.hasMessageContaining("CirclePerimeterDefinition missing required fields");
}
@Test
void shouldThrowWhenJsonIsGarbage() {
String garbageJson = "\"NotAnObjectOrArray\"";
String customError = "Garbage check";
assertThatThrownBy(() -> JacksonUtil.fromString(garbageJson, PerimeterDefinition.class, customError))
.isInstanceOf(IllegalArgumentException.class)
.hasCauseInstanceOf(JsonMappingException.class)
.rootCause()
.hasMessageContaining("Unknown JSON format");
}
@Test
void shouldReturnNullWhenInputIsNull() {
//noinspection ConstantConditions
PerimeterDefinition result = JacksonUtil.fromString(null, PerimeterDefinition.class, "Error");
assertThat(result).isNull();
}
} }

14
msa/edqs/docker/start-tb-edqs.sh

@ -15,11 +15,19 @@
# limitations under the License. # limitations under the License.
# #
CONF_FOLDER=${pkg.installFolder}/conf
jarfile=${pkg.installFolder}/bin/${pkg.name}.jar jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
configfile=${pkg.name}.conf configfile=${pkg.name}.conf
source "${CONF_FOLDER}/${configfile}" CONF_FOLDER="/config"
if [ -d "${CONF_FOLDER}" ]; then
LOGGING_CONFIG="${CONF_FOLDER}/logback.xml"
source "${CONF_FOLDER}/${configfile}"
export LOADER_PATH=${CONF_FOLDER},${LOADER_PATH}
else
CONF_FOLDER="/usr/share/${pkg.name}/conf"
LOGGING_CONFIG="/usr/share/${pkg.name}/conf/logback.xml"
source "${CONF_FOLDER}/${configfile}"
fi
echo "Starting '${project.name}' ..." echo "Starting '${project.name}' ..."
@ -27,5 +35,5 @@ cd ${pkg.installFolder}/bin
exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.edqs.ThingsboardEdqsApplication \ exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.edqs.ThingsboardEdqsApplication \
-Dspring.jpa.hibernate.ddl-auto=none \ -Dspring.jpa.hibernate.ddl-auto=none \
-Dlogging.config=$CONF_FOLDER/logback.xml \ -Dlogging.config=${LOGGING_CONFIG} \
org.springframework.boot.loader.launch.PropertiesLauncher org.springframework.boot.loader.launch.PropertiesLauncher

11
ui-ngx/src/app/core/utils.ts

@ -34,7 +34,7 @@ import {
} from '@shared/models/js-function.models'; } from '@shared/models/js-function.models';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core'; import { SecurityContext } from '@angular/core';
import { AbstractControl, ValidationErrors } from '@angular/forms'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
const varsRegex = /\${([^}]*)}/g; const varsRegex = /\${([^}]*)}/g;
const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
@ -1084,3 +1084,12 @@ export const validateEmail = (control: AbstractControl): ValidationErrors | null
return emailRegex.test(control.value) ? null : {email: true}; return emailRegex.test(control.value) ? null : {email: true};
}; };
export const objectRequired = (): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (value && !isObject(value)) {
return { objectRequired: true };
}
return null;
};
}

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

@ -109,7 +109,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
} }
this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules'); this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules');
this.detailsPanelEnabled = this.pageMode; this.detailsPanelEnabled = this.pageMode;
this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD); this.entityResources = {
helpLinkId: 'alarmRules'
};
this.entityType = EntityType.CALCULATED_FIELD; this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = { this.entityTranslations = {
type: 'alarm-rule.alarm-rule', type: 'alarm-rule.alarm-rule',

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

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../../../../../../scss/constants";
:host { :host {
.filter-title { .filter-title {
padding: 12px 0; padding: 12px 0;
@ -45,11 +47,11 @@
&-label { &-label {
font-size: 15px; font-size: 15px;
font-weight: 400; font-weight: 400;
color: #00695C; color: $tb-primary-color;
padding: 0 8px; padding: 0 8px;
border-radius: 4px; border-radius: 4px;
border: 1px solid rgba(#00695C, 0.32); border: 1px solid rgba($tb-primary-color, 0.32);
background-color: rgba(#00695C, 0.04); background-color: rgba($tb-primary-color, 0.04);
} }
} }

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

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../../../../../../scss/constants";
:host { :host {
.filter-title { .filter-title {
padding: 12px 8px; padding: 12px 8px;
@ -40,11 +42,11 @@
&-label { &-label {
font-size: 15px; font-size: 15px;
font-weight: 400; font-weight: 400;
color: #00695C; color: $tb-primary-color;
padding: 0 8px; padding: 0 8px;
border-radius: 4px; border-radius: 4px;
border: 1px solid rgba(#00695C, 0.32); border: 1px solid rgba($tb-primary-color, 0.32);
background-color: rgba(#00695C, 0.04); background-color: rgba($tb-primary-color, 0.04);
} }
} }
} }

1
ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html

@ -45,6 +45,7 @@
</mat-slide-toggle> </mat-slide-toggle>
<section class="flex gap-3"> <section class="flex gap-3">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1"> <mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1">
<mat-label translate>api-key.expiration-date</mat-label>
<mat-select formControlName="expirationTime" aria-label="Expiration date selector" (selectionChange)="onExpirationDateChange()"> <mat-select formControlName="expirationTime" aria-label="Expiration date selector" (selectionChange)="onExpirationDateChange()">
@for (value of expirationTimeOptions; track value) { @for (value of expirationTimeOptions; track value) {
<mat-option [value]="value"> <mat-option [value]="value">

2
ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts

@ -81,7 +81,7 @@ export class ApiKeysTableConfig extends EntityTableConfig<ApiKeyInfo> {
new DateEntityTableColumn<ApiKeyInfo>('createdTime', 'common.created-time', this.datePipe, '170px'), new DateEntityTableColumn<ApiKeyInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),
new EntityTableColumn<ApiKeyInfo>('description', 'api-key.description', '100%', new EntityTableColumn<ApiKeyInfo>('description', 'api-key.description', '100%',
(entity) => this.customTranslate.transform(entity?.description), () => ({}), true, () => ({}), (entity) => this.customTranslate.transform(entity?.description), () => ({}), true, () => ({}),
(entity) => entity?.description.length > 80 ? this.customTranslate.transform(entity.description) : undefined, false, (entity) => entity?.description.length > 40 ? this.customTranslate.transform(entity.description) : undefined, false,
{ {
name: this.translate.instant('api-key.edit-description'), name: this.translate.instant('api-key.edit-description'),
icon: 'edit', icon: 'edit',

3
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts

@ -124,13 +124,14 @@ export class CalculatedFieldComponent extends EntityComponent<CalculatedFieldsTa
this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true}); this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true});
setTimeout(() => { setTimeout(() => {
this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false});
this.entityForm.get('type').updateValueAndValidity();
}); });
} }
onTestScript(expression?: string): Observable<string> { onTestScript(expression?: string): Observable<string> {
return this.cfFormService.testScript( return this.cfFormService.testScript(
this.entity?.id?.id, this.entity?.id?.id,
this.entityFormValue(), this.entityValue,
this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig),
this.destroyRef, this.destroyRef,
expression expression

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts

@ -186,6 +186,9 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
this.toggleScopeByOutputType(this.outputForm.get('type').value); this.toggleScopeByOutputType(this.outputForm.get('type').value);
this.updatedStrategy(); this.updatedStrategy();
this.updateTimeSeriesTtl(this.outputForm.get('strategy.saveTimeSeries').value); this.updateTimeSeriesTtl(this.outputForm.get('strategy.saveTimeSeries').value);
if (this.outputForm.invalid) {
this.outputForm.updateValueAndValidity();
}
} }
} }

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

@ -143,7 +143,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
this.updatedFormWithScript(); this.updatedFormWithScript();
} }
setTimeout(() => { setTimeout(() => {
this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true, emitEvent: false}); this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
}); });
} }

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

@ -155,6 +155,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); this.simpleConfiguration.patchValue(formValue, {emitEvent: false});
setTimeout(() => { setTimeout(() => {
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
this.simpleConfiguration.get('output').updateValueAndValidity({onlySelf: true});
}); });
} }

1
ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts

@ -218,6 +218,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
createTenantProfile($event: Event, profileName: string) { createTenantProfile($event: Event, profileName: string) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation();
const tenantProfile: TenantProfile = { const tenantProfile: TenantProfile = {
name: profileName name: profileName
}; };

4
ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html

@ -166,11 +166,11 @@
<ng-template matStepLabel> <ng-template matStepLabel>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div translate>widgets.getting-started.tenant-admin.step4.title</div> <div translate>widgets.getting-started.tenant-admin.step4.title</div>
<a *ngIf="matStepper.selectedIndex === 3" mat-button color="primary" routerLink="/profiles/deviceProfiles">{{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }}</a> <a *ngIf="matStepper.selectedIndex === 3" mat-button color="primary" routerLink="/alarms/alarm-rules">{{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }}</a>
</div> </div>
</ng-template> </ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step4.content' | translate | safe: 'html'"></div> <div [innerHTML]="'widgets.getting-started.tenant-admin.step4.content' | translate | safe: 'html'"></div>
<a mat-stroked-button color="primary" href="https://thingsboard.io/docs/getting-started-guides/helloworld/#step-4-configure-alarm-rules" target="_blank"> <a mat-stroked-button color="primary" href="https://thingsboard.io/docs/user-guide/alarm-rules/" target="_blank">
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }}</a> <mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }}</a>
</mat-step> </mat-step>
<mat-step [completed]="gettingStarted.maxSelectedIndex > 3"> <mat-step [completed]="gettingStarted.maxSelectedIndex > 3">

2
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts

@ -216,7 +216,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
} }
}); });
this.twoFaFormGroup.patchValue(processFormValue); this.twoFaFormGroup.patchValue(processFormValue);
this.filterByTenants = isDefined(this.filterByTenants) ? this.filterByTenants : !Array.isArray(settings?.enforcedUsersFilter.tenantProfilesIds); this.filterByTenants = isDefined(this.filterByTenants) ? this.filterByTenants : !Array.isArray(settings?.enforcedUsersFilter?.tenantProfilesIds);
this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').patchValue(this.filterByTenants, {onlySelf: true}); this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').patchValue(this.filterByTenants, {onlySelf: true});
} }

14
ui-ngx/src/app/shared/components/country-autocomplete.component.html

@ -15,14 +15,19 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="countryFormGroup" class="mat-block" subscriptSizing="{{subscriptSizing}}" [appearance]="appearance"> <mat-form-field [formGroup]="countryFormGroup" class="mat-block" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label>{{ labelText }}</mat-label> <mat-label>{{ labelText }}</mat-label>
@if (countryFormGroup.get('country').value?.flag) {
<span matPrefix>
{{ countryFormGroup.get('country').value.flag }}
</span>
}
<input matInput type="text" <input matInput type="text"
#countryInput #countryInput
#autocompleteTrigger="matAutocompleteTrigger"
formControlName="country" formControlName="country"
(focusin)="onFocus()" (focusin)="onFocus()"
(blur)="onTouched()" (blur)="checkInputAndAutoSelect()"
[required]="required"
[matAutocomplete]="countryAutocomplete"> [matAutocomplete]="countryAutocomplete">
<button *ngIf="countryFormGroup.get('country').value && !disabled" <button *ngIf="countryFormGroup.get('country').value && !disabled"
type="button" type="button"
@ -55,4 +60,7 @@
<mat-error *ngIf="countryFormGroup.get('country').hasError('required')"> <mat-error *ngIf="countryFormGroup.get('country').hasError('required')">
{{ requiredText }} {{ requiredText }}
</mat-error> </mat-error>
<mat-error *ngIf="countryFormGroup.get('country').hasError('objectRequired')">
{{ objectRequiredText }}
</mat-error>
</mat-form-field> </mat-form-field>

29
ui-ngx/src/app/shared/components/country-autocomplete.component.scss

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.mat-form-field-appearance-fill {
.mat-mdc-form-field-icon-prefix {
align-self: baseline;
}
}
.mat-mdc-form-field-icon-prefix {
& > * {
color: initial;
padding-left: 8px;
}
}
}

62
ui-ngx/src/app/shared/components/country-autocomplete.component.ts

@ -14,7 +14,18 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import {
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import { Country, CountryData } from '@shared/models/country.models'; import { Country, CountryData } from '@shared/models/country.models';
import { import {
ControlValueAccessor, ControlValueAccessor,
@ -23,14 +34,16 @@ import {
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ValidationErrors, ValidationErrors,
Validator Validator,
Validators
} from '@angular/forms'; } from '@angular/forms';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr, objectRequired } from '@core/utils';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
interface CountrySearchData extends Country { interface CountrySearchData extends Country {
searchText?: string; searchText?: string;
@ -39,6 +52,7 @@ interface CountrySearchData extends Country {
@Component({ @Component({
selector: 'tb-country-autocomplete', selector: 'tb-country-autocomplete',
templateUrl: 'country-autocomplete.component.html', templateUrl: 'country-autocomplete.component.html',
styleUrls: ['./country-autocomplete.component.scss'],
providers: [ providers: [
CountryData, CountryData,
{ {
@ -53,7 +67,7 @@ interface CountrySearchData extends Country {
} }
] ]
}) })
export class CountryAutocompleteComponent implements OnInit, ControlValueAccessor, Validator { export class CountryAutocompleteComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
@Input() @Input()
labelText = this.translate.instant('contact.country'); labelText = this.translate.instant('contact.country');
@ -61,6 +75,9 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
@Input() @Input()
requiredText = this.translate.instant('contact.country-required'); requiredText = this.translate.instant('contact.country-required');
@Input()
objectRequiredText = this.translate.instant('contact.country-object-required');
@Input() @Input()
autocompleteHint: string; autocompleteHint: string;
@ -79,6 +96,8 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
@ViewChild('countryInput', {static: true}) countryInput: ElementRef; @ViewChild('countryInput', {static: true}) countryInput: ElementRef;
@ViewChild('autocompleteTrigger') autocompleteTrigger: MatAutocompleteTrigger;
@Output() @Output()
selectCountryCode = new EventEmitter<string>(); selectCountryCode = new EventEmitter<string>();
@ -103,10 +122,24 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
private countryData: CountryData, private countryData: CountryData,
private translate: TranslateService) { private translate: TranslateService) {
this.countryFormGroup = this.fb.group({ this.countryFormGroup = this.fb.group({
country: [''] country: ['', objectRequired()]
}); });
} }
ngOnChanges(changes: SimpleChanges) {
if (changes.required) {
const requiredChanges = changes.required;
if (requiredChanges.currentValue !== requiredChanges.previousValue) {
if (requiredChanges.currentValue) {
this.countryFormGroup.get('country').addValidators(Validators.required);
} else {
this.countryFormGroup.get('country').removeValidators(Validators.required);
}
this.countryFormGroup.get('country').updateValueAndValidity();
}
}
}
ngOnInit(): void { ngOnInit(): void {
this.filteredCountries = this.countryFormGroup.get('country').valueChanges.pipe( this.filteredCountries = this.countryFormGroup.get('country').valueChanges.pipe(
debounceTime(150), debounceTime(150),
@ -163,7 +196,7 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
} }
displayCountryFn(country?: Country): string | undefined { displayCountryFn(country?: Country): string | undefined {
return country ? `${country.flag} ${country.name}` : undefined; return country ? country.name : undefined;
} }
onFocus() { onFocus() {
@ -173,6 +206,21 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
} }
} }
checkInputAndAutoSelect() {
const control = this.countryFormGroup.get('country');
const value = control.value;
if (value && typeof value === 'string') {
const foundCountry = this.fetchCountries(value);
if (foundCountry.length === 1) {
control.setValue(foundCountry[0]);
this.autocompleteTrigger?.closePanel();
}
}
this.onTouched();
}
textIsNotEmpty(text: string): boolean { textIsNotEmpty(text: string): boolean {
return (text && text.length > 0); return (text && text.length > 0);
} }

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

@ -232,7 +232,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
writeValue(sec: number) { writeValue(sec: number) {
if (sec !== this.modelValue) { if (sec !== this.modelValue) {
if (isDefinedAndNotNull(sec) && isNumeric(sec) && Number(sec) !== 0) { if (isDefinedAndNotNull(sec) && isNumeric(sec) && Number(sec) !== 0) {
this.timeInputForm.patchValue(this.parseTime(sec), {emitEvent: false}); this.timeInputForm.patchValue(this.parseTime(sec), {emitEvent: true});
this.modelValue = sec; this.modelValue = sec;
} else { } else {
this.timeInputForm.patchValue({ this.timeInputForm.patchValue({

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

@ -179,11 +179,11 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxCalculatedFieldsPerEntity: 5, maxCalculatedFieldsPerEntity: 5,
maxArgumentsPerCF: 10, maxArgumentsPerCF: 10,
maxDataPointsPerRollingArg: 1000, maxDataPointsPerRollingArg: 1000,
maxRelationLevelPerCfArgument: 10, maxRelationLevelPerCfArgument: 2,
minAllowedDeduplicationIntervalInSecForCF: 10, minAllowedDeduplicationIntervalInSecForCF: 10,
minAllowedAggregationIntervalInSecForCF: 60, minAllowedAggregationIntervalInSecForCF: 60,
maxRelatedEntitiesToReturnPerCfArgument: 100, maxRelatedEntitiesToReturnPerCfArgument: 100,
minAllowedScheduledUpdateIntervalInSecForCF: 0, minAllowedScheduledUpdateIntervalInSecForCF: 10,
intermediateAggregationIntervalInSecForCF: 300, intermediateAggregationIntervalInSecForCF: 300,
cfReevaluationCheckInterval: 60, cfReevaluationCheckInterval: 60,
alarmsReevaluationInterval: 60, alarmsReevaluationInterval: 60,

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

@ -516,7 +516,7 @@
"within-time": "Within time", "within-time": "Within time",
"within-time-pattern": "Time must be a positive integer.", "within-time-pattern": "Time must be a positive integer.",
"within-time-required": "Time is required.", "within-time-required": "Time is required.",
"force-2fa": "Force two-factor authentication", "force-2fa": "Enforce two-factor authentication",
"enforce-for": "Enforce for" "enforce-for": "Enforce for"
}, },
"jwt": { "jwt": {
@ -1000,7 +1000,8 @@
"delete-api-key-text": "Be careful, after the confirmation key will become unrecoverable.", "delete-api-key-text": "Be careful, after the confirmation key will become unrecoverable.",
"delete-api-keys-title": "Are you sure you want to delete { count, plural, =1 {1 API key} other {# API keys} }?", "delete-api-keys-title": "Are you sure you want to delete { count, plural, =1 {1 API key} other {# API keys} }?",
"delete-api-keys-text": "Be careful, after the confirmation all selected keys will become unrecoverable.", "delete-api-keys-text": "Be careful, after the confirmation all selected keys will become unrecoverable.",
"date": "Date", "expiration-date": "Expiration date",
"date": "date",
"description": "Description", "description": "Description",
"disable": "Disable", "disable": "Disable",
"edit-description": "Edit description", "edit-description": "Edit description",
@ -1665,6 +1666,7 @@
"contact": { "contact": {
"country": "Country", "country": "Country",
"country-required": "Country is required.", "country-required": "Country is required.",
"country-object-required": "Please select a valid country from the list.",
"city": "City", "city": "City",
"state": "State / Province", "state": "State / Province",
"postal-code": "Zip / Postal Code", "postal-code": "Zip / Postal Code",

Loading…
Cancel
Save