From 37e23294d41ba6ba57b1099e5fad78cfe7c5965c Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 12 Jan 2026 14:09:49 +0200 Subject: [PATCH 01/20] UI: Fixed cf pages minor bugs and change default color for operation for alarm rule --- .../filter/alarm-rule-filter-list.component.scss | 8 +++++--- .../alarm-rule-filter-predicate-list.component.scss | 8 +++++--- .../calculated-fields/calculated-field.component.ts | 2 +- .../output/calculated-field-output.component.ts | 2 +- .../propagation-configuration.component.ts | 2 +- .../simple-configuration.component.ts | 1 + .../app/shared/components/time-unit-input.component.ts | 2 +- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss index 985088b568..fea71605c8 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss +++ b/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 * limitations under the License. */ +@import "../../../../../../scss/constants"; + :host { .filter-title { padding: 12px 0; @@ -45,11 +47,11 @@ &-label { font-size: 15px; font-weight: 400; - color: #00695C; + color: $tb-primary-color; padding: 0 8px; border-radius: 4px; - border: 1px solid rgba(#00695C, 0.32); - background-color: rgba(#00695C, 0.04); + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss index 5a6318dde5..e5e6dbeba4 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss +++ b/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 * limitations under the License. */ +@import "../../../../../../scss/constants"; + :host { .filter-title { padding: 12px 8px; @@ -40,11 +42,11 @@ &-label { font-size: 15px; font-weight: 400; - color: #00695C; + color: $tb-primary-color; padding: 0 8px; border-radius: 4px; - border: 1px solid rgba(#00695C, 0.32); - background-color: rgba(#00695C, 0.04); + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts index 20ebe549a2..284c07b9ec 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -121,7 +121,7 @@ export class CalculatedFieldComponent extends EntityComponent { this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 972b74a9f7..c8cb145cfa 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -213,7 +213,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val private updatedFormWithMode(): void { if (this.simpleMode && !this.hiddenName) { - this.outputForm.get('name').enable({emitEvent: false}); + this.outputForm.get('name').enable({emitEvent: true}); } else { this.outputForm.get('name').disable({emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index 7aef195480..bbb0345a1b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -137,7 +137,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, this.updatedFormWithScript(); } setTimeout(() => { - this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true, emitEvent: false}); + this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); }); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index fd86a65621..fd2e0783af 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/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}); setTimeout(() => { this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + this.simpleConfiguration.get('output').updateValueAndValidity({onlySelf: true}); }); } diff --git a/ui-ngx/src/app/shared/components/time-unit-input.component.ts b/ui-ngx/src/app/shared/components/time-unit-input.component.ts index 95e5a3f8e1..ce1c7bd817 100644 --- a/ui-ngx/src/app/shared/components/time-unit-input.component.ts +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.ts @@ -232,7 +232,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, writeValue(sec: number) { if (sec !== this.modelValue) { 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; } else { this.timeInputForm.patchValue({ From bf656d2926aebe035abd8dc458c52b87f6a47ea2 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 13 Jan 2026 10:35:23 +0200 Subject: [PATCH 02/20] created new number utils class --- .../ctx/state/SimpleCalculatedFieldState.java | 4 +-- .../aggregation/function/AvgAggEntry.java | 4 +-- .../aggregation/function/MaxAggEntry.java | 4 +-- .../aggregation/function/MinAggEntry.java | 4 +-- .../aggregation/function/SumAggEntry.java | 4 +-- ...EntityAggregationCalculatedFieldState.java | 4 +-- .../thingsboard/server/utils/NumberUtils.java | 33 +++++++++++++++++++ .../server/utils/NumberUtilsTest.java | 32 ++++++++++++++++++ .../thingsboard/script/api/tbel/TbUtils.java | 12 ------- .../script/api/tbel/TbUtilsTest.java | 7 ---- 10 files changed, 77 insertions(+), 31 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/utils/NumberUtils.java create mode 100644 application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 63f398bbdc..d05276d835 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -22,13 +22,13 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.EqualsAndHashCode; import net.objecthunter.exp4j.Expression; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.utils.NumberUtils; import java.util.Map; @@ -52,7 +52,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { double expressionResult = ctx.evaluateSimpleExpression(expression.get(), this); Output output = ctx.getOutput(); - Object result = TbUtils.roundResult(expressionResult, output.getDecimalsByDefault()); + Object result = NumberUtils.roundResult(expressionResult, output.getDecimalsByDefault()); JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result); return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java index ee06ae1529..09985eab8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; -import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.utils.NumberUtils; import java.math.BigDecimal; import java.math.RoundingMode; @@ -37,7 +37,7 @@ public class AvgAggEntry extends BaseAggEntry { @Override protected Object prepareResult(Integer precision) { double result = sum.divide(BigDecimal.valueOf(count), RoundingMode.HALF_UP).doubleValue(); - return TbUtils.roundResult(result, precision); + return NumberUtils.roundResult(result, precision); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java index b006d8fcd9..0cc3bc4621 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; -import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.utils.NumberUtils; public class MaxAggEntry extends BaseAggEntry { @@ -31,7 +31,7 @@ public class MaxAggEntry extends BaseAggEntry { @Override protected Object prepareResult(Integer precision) { - return TbUtils.roundResult(max, precision); + return NumberUtils.roundResult(max, precision); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java index 2ce561d381..19ea27fb5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; -import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.utils.NumberUtils; public class MinAggEntry extends BaseAggEntry { @@ -31,7 +31,7 @@ public class MinAggEntry extends BaseAggEntry { @Override protected Object prepareResult(Integer precision) { - return TbUtils.roundResult(min, precision); + return NumberUtils.roundResult(min, precision); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java index 3639e36249..9776c977f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; -import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.utils.NumberUtils; import java.math.BigDecimal; @@ -33,7 +33,7 @@ public class SumAggEntry extends BaseAggEntry { @Override protected Object prepareResult(Integer precision) { - return TbUtils.roundResult(sum.doubleValue(), precision); + return NumberUtils.roundResult(sum.doubleValue(), precision); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index 1297a22478..2676461637 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -41,6 +40,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.utils.NumberUtils; import java.time.Instant; import java.time.ZoneId; @@ -291,7 +291,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt ArgumentEntry argumentEntry = entry.getValue(); if (!argumentEntry.isEmpty()) { Object resultValue = argumentEntry.getValue() instanceof Number number - ? TbUtils.roundResult(number.doubleValue(), precision) + ? NumberUtils.roundResult(number.doubleValue(), precision) : argumentEntry.getValue(); metricsNode.put(metricName, JacksonUtil.toString(resultValue)); } diff --git a/application/src/main/java/org/thingsboard/server/utils/NumberUtils.java b/application/src/main/java/org/thingsboard/server/utils/NumberUtils.java new file mode 100644 index 0000000000..e34e7039c9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/NumberUtils.java @@ -0,0 +1,33 @@ +/** + * 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.utils; + +import static org.thingsboard.script.api.tbel.TbUtils.toFixed; +import static org.thingsboard.script.api.tbel.TbUtils.toInt; + +public class NumberUtils { + + public static Object roundResult(double value, Integer precision) { + if (precision == null) { + return value; + } + if (precision.equals(0)) { + return toInt(value); + } + return toFixed(value, precision); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java new file mode 100644 index 0000000000..3708182120 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java @@ -0,0 +1,32 @@ +/** + * 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.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class NumberUtilsTest { + + private final double doubleVal = 1729.1729; + + @Test + public void roundResult() { + Assertions.assertEquals(1729.1729, NumberUtils.roundResult(doubleVal, null)); + Assertions.assertEquals(1729, NumberUtils.roundResult(doubleVal, 0)); + Assertions.assertEquals(1729.17, NumberUtils.roundResult(doubleVal, 2)); + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index c0bdf122c3..6340b02dd0 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -264,8 +264,6 @@ public class TbUtils { float.class, int.class))); parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", double.class))); - parserConfig.addImport("roundResult", new MethodStub(TbUtils.class.getMethod("roundResult", - double.class, Integer.class))); parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN", double.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", @@ -1188,16 +1186,6 @@ public class TbUtils { return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); } - public static Object roundResult(double value, Integer precision) { - if (precision == null) { - return value; - } - if (precision.equals(0)) { - return toInt(value); - } - return toFixed(value, precision); - } - public static boolean isNaN(double value) { return Double.isNaN(value); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 9feeb414dc..01ccd5d579 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1154,13 +1154,6 @@ public class TbUtilsTest { Assertions.assertEquals(28, TbUtils.toInt(28.0)); } - @Test - public void roundResult() { - Assertions.assertEquals(1729.1729, TbUtils.roundResult(doubleVal, null)); - Assertions.assertEquals(1729, TbUtils.roundResult(doubleVal, 0)); - Assertions.assertEquals(1729.17, TbUtils.roundResult(doubleVal, 2)); - } - @Test public void isNaN() { assertFalse(TbUtils.isNaN(doubleVal)); From b30ec4b123f00af5741c3d950009807b892798f1 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 13 Jan 2026 12:34:23 +0200 Subject: [PATCH 03/20] UI: Fixed propagete form --- .../calculated-fields/calculated-field.component.ts | 3 ++- .../components/output/calculated-field-output.component.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts index 284c07b9ec..f9edffa964 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -121,9 +121,10 @@ export class CalculatedFieldComponent extends EntityComponent { this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); + this.entityForm.get('type').updateValueAndValidity(); }); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index c8cb145cfa..a9924909b7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/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.updatedStrategy(); this.updateTimeSeriesTtl(this.outputForm.get('strategy.saveTimeSeries').value); + if (this.outputForm.invalid) { + this.outputForm.updateValueAndValidity(); + } } } @@ -213,7 +216,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val private updatedFormWithMode(): void { if (this.simpleMode && !this.hiddenName) { - this.outputForm.get('name').enable({emitEvent: true}); + this.outputForm.get('name').enable({emitEvent: false}); } else { this.outputForm.get('name').disable({emitEvent: false}); } From 0e9756f8ce90ba24c3cd2f14c183307c1b9b5e30 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 13 Jan 2026 16:46:28 +0200 Subject: [PATCH 04/20] moved number utility method to a new utility class --- .../ctx/state/SimpleCalculatedFieldState.java | 2 +- .../aggregation/function/AvgAggEntry.java | 2 +- .../aggregation/function/MaxAggEntry.java | 2 +- .../aggregation/function/MinAggEntry.java | 2 +- .../aggregation/function/SumAggEntry.java | 2 +- ...EntityAggregationCalculatedFieldState.java | 2 +- .../server/utils/NumberUtilsTest.java | 32 ---------- .../thingsboard/script/api/tbel/TbUtils.java | 27 ++------ .../script/api/tbel/TbUtilsTest.java | 27 -------- .../thingsboard/common/util}/NumberUtils.java | 22 ++++++- .../common/util/NumberUtilsTest.java | 61 +++++++++++++++++++ 11 files changed, 91 insertions(+), 90 deletions(-) delete mode 100644 application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java rename {application/src/main/java/org/thingsboard/server/utils => common/util/src/main/java/org/thingsboard/common/util}/NumberUtils.java (58%) create mode 100644 common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index d05276d835..0ec0a941d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; -import org.thingsboard.server.utils.NumberUtils; +import org.thingsboard.common.util.NumberUtils; import java.util.Map; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java index 09985eab8d..5ee6548f29 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; -import org.thingsboard.server.utils.NumberUtils; +import org.thingsboard.common.util.NumberUtils; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java index 0cc3bc4621..f1054bb326 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; -import org.thingsboard.server.utils.NumberUtils; +import org.thingsboard.common.util.NumberUtils; public class MaxAggEntry extends BaseAggEntry { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java index 19ea27fb5b..fc4257dfcb 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; -import org.thingsboard.server.utils.NumberUtils; +import org.thingsboard.common.util.NumberUtils; public class MinAggEntry extends BaseAggEntry { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java index 9776c977f6..9908757e56 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.cf.ctx.state.aggregation.function; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; -import org.thingsboard.server.utils.NumberUtils; +import org.thingsboard.common.util.NumberUtils; import java.math.BigDecimal; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index 2676461637..c945868576 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -40,7 +40,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; -import org.thingsboard.server.utils.NumberUtils; +import org.thingsboard.common.util.NumberUtils; import java.time.Instant; import java.time.ZoneId; diff --git a/application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java deleted file mode 100644 index 3708182120..0000000000 --- a/application/src/test/java/org/thingsboard/server/utils/NumberUtilsTest.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 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.utils; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class NumberUtilsTest { - - private final double doubleVal = 1729.1729; - - @Test - public void roundResult() { - Assertions.assertEquals(1729.1729, NumberUtils.roundResult(doubleVal, null)); - Assertions.assertEquals(1729, NumberUtils.roundResult(doubleVal, 0)); - Assertions.assertEquals(1729.17, NumberUtils.roundResult(doubleVal, 2)); - } - -} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 6340b02dd0..7fb56a89ff 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -26,6 +26,7 @@ import org.mvel2.execution.ExecutionHashMap; import org.mvel2.execution.ExecutionLinkedHashSet; import org.mvel2.util.MethodStub; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.NumberUtils; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.GeoUtil; import org.thingsboard.common.util.geo.RangeUnit; @@ -35,7 +36,6 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; -import java.math.RoundingMode; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.ByteBuffer; @@ -258,13 +258,13 @@ public class TbUtils { byte[].class, int.class, int.class))); parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", byte[].class, int.class, int.class, boolean.class))); - parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", + parserConfig.addImport("toFixed", new MethodStub(NumberUtils.class.getMethod("toFixed", double.class, int.class))); - parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", + parserConfig.addImport("toFixed", new MethodStub(NumberUtils.class.getMethod("toFixed", float.class, int.class))); - parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", + parserConfig.addImport("toInt", new MethodStub(NumberUtils.class.getMethod("toInt", double.class))); - parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN", + parserConfig.addImport("isNaN", new MethodStub(NumberUtils.class.getMethod("isNaN", double.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", ExecutionContext.class, String.class))); @@ -1174,22 +1174,6 @@ public class TbUtils { return new String(hexChars, StandardCharsets.UTF_8); } - public static double toFixed(double value, int precision) { - return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue(); - } - - public static float toFixed(float value, int precision) { - return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue(); - } - - public static int toInt(double value) { - return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); - } - - public static boolean isNaN(double value) { - return Double.isNaN(value); - } - public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json) { return toFlatMap(ctx, json, new ArrayList<>(), true); } @@ -1208,7 +1192,6 @@ public class TbUtils { return map; } - public static String encodeURI(String uri) { String encoded = URLEncoder.encode(uri, StandardCharsets.UTF_8); for (var entry : mdnEncodingReplacements.entrySet()) { diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 01ccd5d579..bdafeac8a7 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -279,13 +279,6 @@ public class TbUtilsTest { Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev))); } - @Test - public void toFixedFloat() { - float actualF = TbUtils.toFixed(floatVal, 3); - Assertions.assertEquals(1, Float.compare(floatVal, actualF)); - Assertions.assertEquals(0, Float.compare(29.298f, actualF)); - } - @Test public void parseBytesToFloat() { byte[] floatValByte = {0x0A}; @@ -461,13 +454,6 @@ public class TbUtilsTest { Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev))); } - @Test - public void toFixedDouble() { - double actualD = TbUtils.toFixed(doubleVal, 3); - Assertions.assertEquals(-1, Double.compare(doubleVal, actualD)); - Assertions.assertEquals(0, Double.compare(1729.173, actualD)); - } - @Test public void parseBytesToDouble() { byte[] doubleValByte = {0x0A}; @@ -1147,19 +1133,6 @@ public class TbUtilsTest { Assertions.assertEquals(expected, actual); } - @Test - public void toInt() { - Assertions.assertEquals(1729, TbUtils.toInt(doubleVal)); - Assertions.assertEquals(13, TbUtils.toInt(12.8)); - Assertions.assertEquals(28, TbUtils.toInt(28.0)); - } - - @Test - public void isNaN() { - assertFalse(TbUtils.isNaN(doubleVal)); - assertTrue(TbUtils.isNaN(Double.NaN)); - } - @Test public void isInsidePolygon() { // outside the polygon diff --git a/application/src/main/java/org/thingsboard/server/utils/NumberUtils.java b/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java similarity index 58% rename from application/src/main/java/org/thingsboard/server/utils/NumberUtils.java rename to common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java index e34e7039c9..6681557c87 100644 --- a/application/src/main/java/org/thingsboard/server/utils/NumberUtils.java +++ b/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java @@ -13,13 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.utils; +package org.thingsboard.common.util; -import static org.thingsboard.script.api.tbel.TbUtils.toFixed; -import static org.thingsboard.script.api.tbel.TbUtils.toInt; +import java.math.BigDecimal; +import java.math.RoundingMode; public class NumberUtils { + public static boolean isNaN(double value) { + return Double.isNaN(value); + } + + public static double toFixed(double value, int precision) { + return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue(); + } + + public static float toFixed(float value, int precision) { + return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue(); + } + + public static int toInt(double value) { + return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); + } + public static Object roundResult(double value, Integer precision) { if (precision == null) { return value; diff --git a/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java b/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java new file mode 100644 index 0000000000..1a1bf326e6 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java @@ -0,0 +1,61 @@ +/** + * 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.common.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NumberUtilsTest { + + private final Float floatVal = 29.29824f; + private final double doubleVal = 1729.1729; + + @Test + public void isNaN() { + assertThat(NumberUtils.isNaN(doubleVal)).isFalse(); + assertThat(NumberUtils.isNaN(Double.NaN)).isTrue(); + } + + @Test + public void toFixedFloat() { + float actualF = NumberUtils.toFixed(floatVal, 3); + assertThat(Float.compare(floatVal, actualF)).isEqualTo(1); + assertThat(Float.compare(29.298f, actualF)).isEqualTo(0); + } + + @Test + public void toFixedDouble() { + double actualD = NumberUtils.toFixed(doubleVal, 3); + assertThat(Double.compare(doubleVal, actualD)).isEqualTo(-1); + assertThat(Double.compare(1729.173, actualD)).isEqualTo(0); + } + + @Test + public void toInt() { + assertThat(NumberUtils.toInt(doubleVal)).isEqualTo(1729); + assertThat(NumberUtils.toInt(12.8)).isEqualTo(13); + assertThat(NumberUtils.toInt(28.0)).isEqualTo(28); + } + + @Test + public void roundResult() { + assertThat(NumberUtils.roundResult(doubleVal, null)).isEqualTo(1729.1729); + assertThat(NumberUtils.roundResult(doubleVal, 0)).isEqualTo(1729); + assertThat(NumberUtils.roundResult(doubleVal, 2)).isEqualTo(1729.17); + } + +} From 38d4f23c5eb657483c0b21b927e15233941923ff Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 13 Jan 2026 17:12:41 +0200 Subject: [PATCH 05/20] Fix CONF_FOLDER for start-tb-edqs.sh in k8s --- msa/edqs/docker/start-tb-edqs.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/msa/edqs/docker/start-tb-edqs.sh b/msa/edqs/docker/start-tb-edqs.sh index 40970ead21..4968f5b866 100755 --- a/msa/edqs/docker/start-tb-edqs.sh +++ b/msa/edqs/docker/start-tb-edqs.sh @@ -15,11 +15,19 @@ # limitations under the License. # -CONF_FOLDER=${pkg.installFolder}/conf jarfile=${pkg.installFolder}/bin/${pkg.name}.jar 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}' ..." @@ -27,5 +35,5 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.edqs.ThingsboardEdqsApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ - -Dlogging.config=$CONF_FOLDER/logback.xml \ + -Dlogging.config=${LOGGING_CONFIG} \ org.springframework.boot.loader.launch.PropertiesLauncher From 32f7c2abf744caeddd45e3eed843f145176ead45 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 13 Jan 2026 17:44:31 +0200 Subject: [PATCH 06/20] fixed smsEnabled property handling for tenant profile configuration --- .../DefaultTbApiUsageStateServiceTest.java | 84 +++++++++++++++++++ .../usagerecord/ApiUsageStateServiceImpl.java | 28 +++++-- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 7530c646f4..a358c0f12c 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -40,6 +41,7 @@ import java.lang.reflect.Field; import java.time.LocalDate; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -47,7 +49,10 @@ import static java.time.ZoneOffset.UTC; import static java.time.temporal.ChronoField.DAY_OF_MONTH; import static java.time.temporal.ChronoUnit.MONTHS; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; +import static org.thingsboard.server.common.data.ApiFeature.SMS; +import static org.thingsboard.server.common.data.ApiUsageRecordKey.SMS_EXEC_COUNT; @DaoSqlTest public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { @@ -157,6 +162,85 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth); } + @Test + public void checkTenantCreatedWithSmsDisabledApiUsage() throws Exception { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Test profile"); + TenantProfileData tenantProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() + .smsEnabled(false) + .build(); + + tenantProfileData.setConfiguration(config); + tenantProfile.setProfileData(tenantProfileData); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + Tenant tenant = new Tenant(); + tenant.setTitle("My test tenant"); + tenant.setTenantProfileId(tenantProfile.getId()); + tenant = saveTenant(tenant); + + ApiUsageState apiUsageState = apiUsageStateService.findApiUsageStateByEntityId(tenant.getId()); + assertThat(apiUsageState.getSmsExecState()).isEqualTo(ApiUsageStateValue.DISABLED); + + TenantId finalTenantId = tenant.getId(); + ApiUsageStateId finalApiUsageStateId = apiUsageState.getId(); + + + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Optional smsApiState = tsService.findLatest(finalTenantId, finalApiUsageStateId, SMS.getApiStateKey()).get(); + return smsApiState.isPresent() && smsApiState.get().getValueAsString().equals(ApiUsageStateValue.DISABLED.name()); + }); + + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Optional smsApiState = tsService.findLatest(finalTenantId, finalApiUsageStateId, SMS_EXEC_COUNT.getApiLimitKey()).get(); + return smsApiState.isPresent() && smsApiState.get().getLongValue().get().equals(0L); + }); + + // enable SMS and check that the ApiUsageState is updated accordingly + config = DefaultTenantProfileConfiguration.builder() + .smsEnabled(true) + .maxSms(10) + .build(); + tenantProfileData.setConfiguration(config); + tenantProfile.setProfileData(tenantProfileData); + doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + await().atMost(5, TimeUnit.SECONDS).until(() -> + apiUsageStateService.findApiUsageStateByEntityId(finalTenantId).getSmsExecState() == ApiUsageStateValue.ENABLED); + + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Optional smsApiState = tsService.findLatest(finalTenantId, finalApiUsageStateId, SMS.getApiStateKey()).get(); + return smsApiState.isPresent() && smsApiState.get().getValueAsString().equals(ApiUsageStateValue.ENABLED.name()); + }); + + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Optional smsApiState = tsService.findLatest(finalTenantId, finalApiUsageStateId, SMS_EXEC_COUNT.getApiLimitKey()).get(); + return smsApiState.isPresent() && smsApiState.get().getLongValue().get().equals(10L); + }); + + //disable SMS and check that the ApiUsageState is updated accordingly + config = DefaultTenantProfileConfiguration.builder() + .smsEnabled(false) + .build(); + tenantProfileData.setConfiguration(config); + tenantProfile.setProfileData(tenantProfileData); + doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + await().atMost(5, TimeUnit.SECONDS).until(() -> + apiUsageStateService.findApiUsageStateByEntityId(finalTenantId).getSmsExecState() == ApiUsageStateValue.DISABLED); + + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Optional smsApiState = tsService.findLatest(finalTenantId, finalApiUsageStateId, SMS.getApiStateKey()).get(); + return smsApiState.isPresent() && smsApiState.get().getValueAsString().equals(ApiUsageStateValue.DISABLED.name()); + }); + + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Optional smsApiState = tsService.findLatest(finalTenantId, finalApiUsageStateId, SMS_EXEC_COUNT.getApiLimitKey()).get(); + return smsApiState.isPresent() && smsApiState.get().getLongValue().get().equals(0L); + }); + } + private TenantProfile createTenantProfile() { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName("Tenant Profile"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index b97c81c612..7f0e507f5f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -101,6 +101,21 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A entityId = Objects.requireNonNullElse(entityId, tenantId); log.trace("Executing createDefaultUsageRecord [{}]", entityId); validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + + ApiUsageStateValue smsApiUsageState = ApiUsageStateValue.ENABLED; + + DefaultTenantProfileConfiguration configuration = null; + if (entityId.getEntityType() == EntityType.TENANT && !entityId.equals(TenantId.SYS_TENANT_ID)) { + tenantId = (TenantId) entityId; + Tenant tenant = tenantService.findTenantById(tenantId); + TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenant.getTenantProfileId().getId()); + configuration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); + + if (configuration.getSmsEnabled() != null && !configuration.getSmsEnabled()) { + smsApiUsageState = ApiUsageStateValue.DISABLED; + } + } + ApiUsageState apiUsageState = new ApiUsageState(); apiUsageState.setTenantId(tenantId); apiUsageState.setEntityId(entityId); @@ -109,7 +124,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); - apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setSmsExecState(smsApiUsageState); apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); apiUsageStateValidator.validate(apiUsageState, ApiUsageState::getTenantId); @@ -138,17 +153,12 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A apiUsageStates.add(new BasicTsKvEntry(saved.getCreatedTime(), new StringDataEntry(ApiFeature.EMAIL.getApiStateKey(), ApiUsageStateValue.ENABLED.name()))); apiUsageStates.add(new BasicTsKvEntry(saved.getCreatedTime(), - new StringDataEntry(ApiFeature.SMS.getApiStateKey(), ApiUsageStateValue.ENABLED.name()))); + new StringDataEntry(ApiFeature.SMS.getApiStateKey(), smsApiUsageState.name()))); apiUsageStates.add(new BasicTsKvEntry(saved.getCreatedTime(), new StringDataEntry(ApiFeature.ALARM.getApiStateKey(), ApiUsageStateValue.ENABLED.name()))); tsService.save(tenantId, saved.getId(), apiUsageStates, 0L); - if (entityId.getEntityType() == EntityType.TENANT && !entityId.equals(TenantId.SYS_TENANT_ID)) { - tenantId = (TenantId) entityId; - Tenant tenant = tenantService.findTenantById(tenantId); - TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenant.getTenantProfileId().getId()); - TenantProfileConfiguration configuration = tenantProfile.getProfileData().getConfiguration(); - + if (configuration != null) { List profileThresholds = new ArrayList<>(); for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { if (key.getApiLimitKey() == null) continue; From ec128f3e855e39259f05329471c626c0f3818a7b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 13 Jan 2026 18:28:26 +0200 Subject: [PATCH 07/20] null value for smsEnabled is interpreted as enabled on backend --- .../tenant/default-tenant-profile-configuration.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index fe8c4af871..4f1efa32d2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DefaultTenantProfileConfiguration, TenantProfileConfiguration } from '@shared/models/tenant.model'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isUndefinedOrNull} from '@core/utils'; import { RateLimitsType } from './rate-limits/rate-limits.models'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; @@ -179,6 +179,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA writeValue(value: DefaultTenantProfileConfiguration | null): void { if (isDefinedAndNotNull(value)) { + if (isUndefinedOrNull(value.smsEnabled)) { + value.smsEnabled = true; + } this.maxSmsValidation(value.smsEnabled); this.defaultTenantProfileConfigurationFormGroup.patchValue(value, {emitEvent: false}); } From 6f815c76a64ffe039247dd01361f618c0c8471f6 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 13 Jan 2026 19:40:47 +0200 Subject: [PATCH 08/20] Handling minAllowedScheduledUpdateIntervalInSecForCF and minAllowedDeduplicationIntervalInSecForCF in running CFs + updated default values in profile and upgrade --- .../main/data/upgrade/basic/schema_update.sql | 4 +-- .../controller/TenantProfileController.java | 4 +-- .../cf/ctx/state/CalculatedFieldCtx.java | 10 ++++--- .../service/cf/ctx/state/HasEntityLimit.java | 29 +++++++++++++++++++ ...titiesAggregationCalculatedFieldState.java | 8 +++-- .../RelatedEntitiesArgumentEntry.java | 18 ++++-------- .../propagation/PropagationArgumentEntry.java | 10 ++----- .../DefaultTenantProfileConfiguration.java | 8 ++--- ui-ngx/src/app/shared/models/tenant.model.ts | 4 +-- 9 files changed, 59 insertions(+), 36 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 1700b25dbb..66243461e2 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -21,8 +21,8 @@ SET profile_data = jsonb_set( profile_data, '{configuration}', jsonb_build_object( - 'minAllowedScheduledUpdateIntervalInSecForCF', 60, - 'maxRelationLevelPerCfArgument', 10, + 'minAllowedScheduledUpdateIntervalInSecForCF', 10, + 'maxRelationLevelPerCfArgument', 2, 'maxRelatedEntitiesToReturnPerCfArgument', 100, 'minAllowedDeduplicationIntervalInSecForCF', 10, 'minAllowedAggregationIntervalInSecForCF', 60, diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index ee73b3c1a7..c0f7b9eb40 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -161,8 +161,8 @@ public class TenantProfileController extends BaseController { " \"warnThreshold\": 0,\n" + " \"maxCalculatedFieldsPerEntity\": 5,\n" + " \"maxArgumentsPerCF\": 10,\n" + - " \"minAllowedScheduledUpdateIntervalInSecForCF\": 60,\n" + - " \"maxRelationLevelPerCfArgument\": 10,\n" + + " \"minAllowedScheduledUpdateIntervalInSecForCF\": 10,\n" + + " \"maxRelationLevelPerCfArgument\": 2,\n" + " \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" + " \"maxDataPointsPerRollingArg\": 1000,\n" + " \"maxStateSizeInKBytes\": 32,\n" + diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index e5215eca28..84a0ab008f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -129,6 +129,8 @@ public class CalculatedFieldCtx implements Closeable { private long cfCheckReevaluationIntervalMillis; private long alarmReevaluationIntervalMillis; private long maxRelatedEntitiesPerCfArgument; + private long minScheduledUpdateIntervalMillis; + private long minDeduplicationIntervalMillis; private Argument propagationArgument; private boolean applyExpressionForResolvedArguments; @@ -312,6 +314,8 @@ public class CalculatedFieldCtx implements Closeable { this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getCfReevaluationCheckInterval()); this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getAlarmsReevaluationInterval()); 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() { - return CalculatedFieldType.GEOFENCING == cfType - || CalculatedFieldType.PROPAGATION == cfType - || CalculatedFieldType.RELATED_ENTITIES_AGGREGATION == cfType; + return cfHasRelationPathQuerySource; } public boolean shouldFetchRelatedEntities(CalculatedFieldState state) { @@ -781,7 +783,7 @@ public class CalculatedFieldCtx implements Closeable { if (scheduledRefreshSupported.getLastScheduledRefreshTs() == DEFAULT_LAST_UPDATE_TS) { return true; } - return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - Math.max(scheduledUpdateIntervalMillis, minScheduledUpdateIntervalMillis); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java new file mode 100644 index 0000000000..7d5bc47cc0 --- /dev/null +++ b/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." + ); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index cb64e03d90..868e8a15ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/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() { - ScheduledFuture future = ctx.scheduleReevaluation(deduplicationIntervalMs, actorCtx); + ScheduledFuture future = ctx.scheduleReevaluation(getEnforcedDeduplicationIntervalMillis(), actorCtx); if (future != null) { reevaluationFuture = future; } @@ -209,11 +209,15 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat } private boolean shouldRecalculate() { - boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - deduplicationIntervalMs; + boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - getEnforcedDeduplicationIntervalMillis(); boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs; return intervalPassed && argsUpdatedDuringInterval; } + private long getEnforcedDeduplicationIntervalMillis() { + return Math.max(deduplicationIntervalMs, ctx.getMinDeduplicationIntervalMillis()); + } + private Map> prepareInputs() { Map> inputs = new HashMap<>(); for (Map.Entry argEntry : arguments.entrySet()) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java index 7392ff1e0f..9cd40e4250 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java @@ -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.ArgumentEntryType; 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.SingleValueArgumentEntry; @@ -34,7 +35,7 @@ import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldSta @Data @AllArgsConstructor -public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs { +public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs, HasEntityLimit { private final Map entityInputs; @@ -66,18 +67,18 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs @Override public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { - checkRelatedEntitiesNumber(ctx); + checkEntityLimit(entityInputs.size(), ctx); entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); } else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { if (entry.isForceResetPrevious()) { - checkRelatedEntitiesNumber(ctx); + checkEntityLimit(entityInputs.size(), ctx); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); } else { ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); if (argumentEntry != null) { argumentEntry.updateEntry(singleValueArgumentEntry, ctx); } else { - checkRelatedEntitiesNumber(ctx); + checkEntityLimit(entityInputs.size(), ctx); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); } } @@ -87,15 +88,6 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs 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 public boolean isEmpty() { return entityInputs.isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java index c3b5951da6..8fdc105168 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java +++ b/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.ArgumentEntryType; 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.Collection; @@ -30,7 +31,7 @@ import java.util.List; import java.util.Set; @Data -public class PropagationArgumentEntry implements ArgumentEntry { +public class PropagationArgumentEntry implements ArgumentEntry, HasEntityLimit { private Set entityIds; private transient List added; @@ -93,12 +94,7 @@ public class PropagationArgumentEntry implements ArgumentEntry { private boolean checkAdded(Collection updatedIds, CalculatedFieldCtx ctx) { 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." - ); - } + checkEntityLimit(entityIds.size(), ctx); if (entityIds.add(id)) { if (added == null) { added = new ArrayList<>(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index dd3c7b1a7d..a04816365b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -172,12 +172,12 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; - @Schema(example = "60") - private int minAllowedScheduledUpdateIntervalInSecForCF = 60; - @Builder.Default @Schema(example = "10") + private int minAllowedScheduledUpdateIntervalInSecForCF = 10; + @Builder.Default + @Schema(example = "2") @Positive - private int maxRelationLevelPerCfArgument = 10; + private int maxRelationLevelPerCfArgument = 2; @Builder.Default @Schema(example = "100") @Positive diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index bf68b7ea3d..262ecfc62a 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -179,11 +179,11 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxCalculatedFieldsPerEntity: 5, maxArgumentsPerCF: 10, maxDataPointsPerRollingArg: 1000, - maxRelationLevelPerCfArgument: 10, + maxRelationLevelPerCfArgument: 2, minAllowedDeduplicationIntervalInSecForCF: 10, minAllowedAggregationIntervalInSecForCF: 60, maxRelatedEntitiesToReturnPerCfArgument: 100, - minAllowedScheduledUpdateIntervalInSecForCF: 0, + minAllowedScheduledUpdateIntervalInSecForCF: 10, intermediateAggregationIntervalInSecForCF: 300, cfReevaluationCheckInterval: 60, alarmsReevaluationInterval: 60, From 48c31e47b9743baedc64424d69130430fd41e1e7 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 14 Jan 2026 12:59:23 +0200 Subject: [PATCH 09/20] UI: Fixed autofill country autocomplete and improved validation --- ui-ngx/src/app/core/utils.ts | 14 ++++++- .../country-autocomplete.component.html | 13 ++++++- .../country-autocomplete.component.scss | 27 +++++++++++++ .../country-autocomplete.component.ts | 39 ++++++++++++++++--- .../assets/locale/locale.constant-en_US.json | 1 + 5 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/country-autocomplete.component.scss diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index f1a0671a52..e0665d4eb1 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -27,13 +27,14 @@ import { serverErrorCodesTranslations } from '@shared/models/constants'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { CompiledTbFunction, - compileTbFunction, GenericFunction, + compileTbFunction, + GenericFunction, isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; import { DomSanitizer } from '@angular/platform-browser'; import { SecurityContext } from '@angular/core'; -import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; const varsRegex = /\${([^}]*)}/g; const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; @@ -998,3 +999,12 @@ export const validateEmail = (control: AbstractControl): ValidationErrors | null return emailRegex.test(control.value) ? null : {email: true}; }; +export const requireMatch = (): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (value && typeof value === 'string') { + return { requireMatch: { value: value } }; + } + return null; + }; +} diff --git a/ui-ngx/src/app/shared/components/country-autocomplete.component.html b/ui-ngx/src/app/shared/components/country-autocomplete.component.html index 9b3857cafb..61a874431a 100644 --- a/ui-ngx/src/app/shared/components/country-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/country-autocomplete.component.html @@ -15,13 +15,19 @@ limitations under the License. --> - + {{ labelText }} + @if (countryFormGroup.get('country').value?.flag) { + + {{ countryFormGroup.get('country').value.flag }} + + }