Browse Source

Merge remote-tracking branch 'upstream/master' into api-key-rest-client

pull/14529/head
Andrii Landiak 10 months ago
parent
commit
b9c4bcd220
  1. 10
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  2. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  3. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java
  4. 43
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  5. 50
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  6. 22
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java
  7. 9
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  8. 18
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java
  9. 11
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
  10. 1
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java
  11. 85
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  12. 6
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  13. 22
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java
  14. 11
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java
  15. 21
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java
  16. 4
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java
  17. 21
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java
  18. 11
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java
  19. 202
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java
  20. 2
      common/queue/pom.xml
  21. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java
  22. 7
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java
  23. 68
      dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java
  24. 6
      dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java
  25. 111
      dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java
  26. 289
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java
  27. 2
      msa/js-executor/package.json
  28. 14
      msa/js-executor/yarn.lock
  29. 2
      msa/web-ui/package.json
  30. 14
      msa/web-ui/yarn.lock
  31. 14
      pom.xml
  32. 42
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  33. 2
      rule-engine/rule-engine-components/pom.xml
  34. 4
      tools/pom.xml
  35. 66
      ui-ngx/patches/@angular+common+18.2.13.patch
  36. 88
      ui-ngx/patches/@angular+compiler+18.2.13.patch
  37. 10
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  38. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss
  39. 40
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  40. 16
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  41. 205
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  42. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  43. 23
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html
  44. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  45. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
  46. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
  47. 11
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  48. 29
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  49. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  50. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  51. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss
  52. 77
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts
  53. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts
  54. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
  55. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html
  56. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts
  57. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  58. 15
      ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html
  59. 0
      ui-ngx/src/app/modules/home/dialogs/events-dialog.component.scss
  60. 72
      ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts
  61. 1
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  62. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  63. 1
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  64. 37
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  65. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -159,10 +159,14 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else {
state.setCtx(ctx, actorCtx);
}
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
if (msg.getStateAction() != StateAction.REFRESH_CTX) {
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
}
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
msg.getCallback().onSuccess();
}
} catch (Exception e) {
log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e);

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

@ -462,6 +462,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
stateAction = StateAction.REINIT; // refetch arguments, call state.init, then calculate
} else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) {
stateAction = StateAction.REPROCESS; // call state.setCtx, then calculate
} else if (newCfCtx.hasRefreshContextOnlyChanges(oldCfCtx)) {
stateAction = StateAction.REFRESH_CTX;
} else {
callback.onSuccess();
return;

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

@ -39,6 +39,7 @@ public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg
INIT,
REINIT,
RECREATE,
REPROCESS
REPROCESS,
REFRESH_CTX
}
}

43
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -26,8 +26,6 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState;
import org.thingsboard.server.utils.CalculatedFieldUtils;
import java.io.Closeable;
@ -41,7 +39,7 @@ import java.util.stream.Collectors;
@Getter
public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable {
protected static final long DEFAULT_LAST_UPDATE_TS = -1L;
public static final long DEFAULT_LAST_UPDATE_TS = -1L;
protected final EntityId entityId;
protected CalculatedFieldCtx ctx;
@ -103,7 +101,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
updatedArguments = new HashMap<>(argumentValues.size());
}
updatedArguments.put(key, newEntry);
updateLastUpdateTimestamp(newEntry);
}
}
@ -161,23 +158,29 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
return resultNode;
}
private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp;
if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
newTs = singleValueArgumentEntry.getTs();
} else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) {
Map.Entry<Long, Double> lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry();
newTs = (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS;
} else if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) {
newTs = relatedEntitiesArgumentEntry.getEntityInputs().values().stream()
.mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : DEFAULT_LAST_UPDATE_TS)
.max()
.orElse(DEFAULT_LAST_UPDATE_TS);
} else if (entry instanceof GeofencingArgumentEntry geofencingArgumentEntry) {
newTs = geofencingArgumentEntry.getZoneStates().values().stream()
.mapToLong(GeofencingZoneState::getTs).max().orElse(DEFAULT_LAST_UPDATE_TS);
public long getLatestTimestamp() {
long latestTs = DEFAULT_LAST_UPDATE_TS;
boolean allDefault = arguments.values().stream().allMatch(entry -> {
if (entry instanceof SingleValueArgumentEntry single) {
return single.isDefaultValue();
}
return false;
});
for (ArgumentEntry entry : arguments.values()) {
if (entry instanceof SingleValueArgumentEntry single) {
if (allDefault) {
latestTs = Math.max(latestTs, single.getTs());
} else if (!single.isDefaultValue()) {
latestTs = Math.max(latestTs, single.getTs());
}
} else if (entry instanceof HasLatestTs hasLatestTsEntry) {
latestTs = Math.max(latestTs, hasLatestTsEntry.getLatestTs());
}
}
this.latestTimestamp = Math.max(this.latestTimestamp, newTs);
return latestTs;
}
protected ReadinessStatus checkReadiness(List<String> requiredArguments, Map<String, ArgumentEntry> currentArguments) {

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

@ -636,16 +636,33 @@ public class CalculatedFieldCtx implements Closeable {
return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId);
}
public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation
var thisOutputStrategy = calculatedField.getConfiguration().getOutput().getStrategy();
var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy();
if (thisOutputStrategy.hasRefreshContextOnlyChanges(otherOutputStrategy)) {
return true;
}
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
if (thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult()) {
return true;
}
}
return false;
}
public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly
if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) {
return true;
}
if (!Objects.equals(output, other.output)) {
if (output.hasContextOnlyChanges(other.output)) {
return true;
}
if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig
&& thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) {
&& other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig
&& thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) {
return true;
}
if (cfType == CalculatedFieldType.ALARM) {
@ -667,18 +684,19 @@ public class CalculatedFieldCtx implements Closeable {
return true;
}
if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig
&& (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec()
&& other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig
&& (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec()
|| !thisConfig.getMetrics().equals(otherConfig.getMetrics())
|| thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs())) {
return true;
}
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics());
boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark());
boolean produceIntermediateResultChanged = thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult();
return metricsChanged || watermarkChanged || produceIntermediateResultChanged;
if (metricsChanged || watermarkChanged) {
return true;
}
}
return false;
}
@ -711,7 +729,7 @@ public class CalculatedFieldCtx implements Closeable {
private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
&& other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups());
}
return false;
@ -719,7 +737,7 @@ public class CalculatedFieldCtx implements Closeable {
private boolean hasRelatedEntitiesAggregationConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) {
&& other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) {
return !thisConfig.getRelation().equals(otherConfig.getRelation());
}
return false;
@ -727,7 +745,7 @@ public class CalculatedFieldCtx implements Closeable {
private boolean hasEntityAggregationConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
&& other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
return !thisConfig.getInterval().equals(otherConfig.getInterval());
}
return false;
@ -752,7 +770,7 @@ public class CalculatedFieldCtx implements Closeable {
yield true;
}
yield geofencingState.getLastDynamicArgumentsRefreshTs() <
System.currentTimeMillis() - scheduledUpdateIntervalMillis;
System.currentTimeMillis() - scheduledUpdateIntervalMillis;
}
default -> false;
};
@ -796,10 +814,10 @@ public class CalculatedFieldCtx implements Closeable {
@Override
public String toString() {
return "CalculatedFieldCtx{" +
"cfId=" + cfId +
", cfType=" + cfType +
", entityId=" + entityId +
'}';
"cfId=" + cfId +
", cfType=" + cfType +
", entityId=" + entityId +
'}';
}
}

22
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2025 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 HasLatestTs {
long getLatestTs();
}

9
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java

@ -31,11 +31,13 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class TsRollingArgumentEntry implements ArgumentEntry {
public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs {
private Integer limit;
private Long timeWindow;
@ -83,6 +85,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
return tsRecords;
}
public long getLatestTs() {
var lastEntry = tsRecords.lastEntry();
return (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS;
}
@Override
public TbelCfArg toTbelCfArg() {
List<TbelCfTsDoubleVal> values = new ArrayList<>(tsRecords.size());

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

@ -23,14 +23,17 @@ import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
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.HasLatestTs;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.util.Map;
import java.util.stream.Collectors;
import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS;
@Data
@AllArgsConstructor
public class RelatedEntitiesArgumentEntry implements ArgumentEntry {
public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs {
private final Map<EntityId, ArgumentEntry> entityInputs;
@ -46,6 +49,19 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry {
return entityInputs;
}
@Override
public long getLatestTs() {
long latestTs = DEFAULT_LAST_UPDATE_TS;
for (ArgumentEntry entry : entityInputs.values()) {
if (entry instanceof SingleValueArgumentEntry single) {
if (!single.isDefaultValue()) {
latestTs = Math.max(latestTs, single.getTs());
}
}
}
return latestTs;
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) {

11
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java

@ -25,13 +25,16 @@ import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos;
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.HasLatestTs;
import java.util.Map;
import java.util.stream.Collectors;
import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS;
@Data
@Slf4j
public class GeofencingArgumentEntry implements ArgumentEntry {
public class GeofencingArgumentEntry implements ArgumentEntry, HasLatestTs {
private Map<EntityId, GeofencingZoneState> zoneStates;
@ -58,6 +61,12 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
return zoneStates;
}
@Override
public long getLatestTs() {
return zoneStates.values().stream()
.mapToLong(GeofencingZoneState::getTs).max().orElse(DEFAULT_LAST_UPDATE_TS);
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {

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

@ -104,7 +104,6 @@ public class CalculatedFieldArgumentUtils {
public static TsKvEntry createDefaultTsKvEntry(Argument argument, long ts) {
return new BasicTsKvEntry(ts, createDefaultKvEntry(argument), DEFAULT_VERSION);
}
public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) {
return new BaseAttributeKvEntry(createDefaultKvEntry(argument), ts, DEFAULT_VERSION);
}

85
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -580,6 +580,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
@Test
public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis() - 300000L;
postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts));
@ -614,6 +615,90 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
@Test
public void testSimpleCalculatedFieldWhenUseLatestTsIsTrueAndDefaultArguments() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("a + b + c");
calculatedField.setDebugSettings(DebugSettings.all());
calculatedField.setConfigurationVersion(1);
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument1 = new Argument();
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null);
argument1.setRefEntityKey(refEntityKey1);
argument1.setDefaultValue("100");
Argument argument2 = new Argument();
ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("b", ArgumentType.TS_LATEST, null);
argument2.setRefEntityKey(refEntityKey2);
argument2.setDefaultValue("200");
Argument argument3 = new Argument();
ReferencedEntityKey refEntityKey3 = new ReferencedEntityKey("c", ArgumentType.TS_LATEST, null);
argument3.setRefEntityKey(refEntityKey3);
argument3.setDefaultValue("300");
config.setArguments(Map.of("a", argument1, "b", argument2, "c", argument3));
config.setExpression("a + b + c");
TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("d");
output.setDecimalsByDefault(0);
config.setOutput(output);
config.setUseLatestTs(true);
calculatedField.setConfiguration(config);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> perform initial calculation with default arguments").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode d = getLatestTelemetry(testDevice.getId(), "d");
assertThat(d).isNotNull();
assertThat(d.get("d").get(0).get("value").asText()).isEqualTo("600");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}"));
await().alias("update telemetry -> save result with ts of 'a' argument").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d", "a");
assertThat(keys).isNotNull();
String aTs = keys.get("a").get(0).get("ts").asText();
assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(aTs);
assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("510");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":20}"));
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"c\":30}"));
await().alias("update telemetry -> save result with latest ts of updated arguments").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d");
assertThat(keys).isNotNull();
assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("60");
});
String latestTs = getLatestTelemetry(testDevice.getId(), "d").get("d").get(0).get("ts").asText();
doDelete("/api/plugins/telemetry/DEVICE/" + testDevice.getId() + "/timeseries/delete?keys=b&deleteAllDataForKeys=true").andExpect(status().isOk());
await().alias("delete telemetry -> save result with previous latest ts and default argument").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d");
assertThat(keys).isNotNull();
assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(latestTs);
assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("240");
});
}
@Test
public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");

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

@ -247,7 +247,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo(
assertThat(result.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "ENTERED")
.put("allowedZonesStatus", "INSIDE")
@ -329,7 +329,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo(
assertThat(result.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED")
);
@ -406,7 +406,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo(
assertThat(result.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesStatus", "INSIDE")
.put("restrictedZonesStatus", "OUTSIDE")

22
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java

@ -35,4 +35,26 @@ public class AttributesImmediateOutputStrategy implements AttributesOutputStrate
public OutputStrategyType getType() {
return OutputStrategyType.IMMEDIATE;
}
@Override
public boolean hasContextOnlyChanges(OutputStrategy other) {
if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) {
return true;
}
boolean saveTimeSeriesUpdated = saveAttribute != otherStrategy.isSaveAttribute();
boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate();
boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs();
return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated;
}
@Override
public boolean hasRefreshContextOnlyChanges(OutputStrategy other) {
if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) {
return true;
}
boolean updateAttrOnValueChangedChanged = updateAttributesOnlyOnValueChange != otherStrategy.isUpdateAttributesOnlyOnValueChange();
boolean sendAttrUpdatedNotificationChanged = sendAttributesUpdatedNotification != otherStrategy.isSendAttributesUpdatedNotification();
return updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged;
}
}

11
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java

@ -26,4 +26,15 @@ public class AttributesRuleChainOutputStrategy implements AttributesOutputStrate
public OutputStrategyType getType() {
return OutputStrategyType.RULE_CHAIN;
}
@Override
public boolean hasContextOnlyChanges(OutputStrategy other) {
return !(other instanceof AttributesRuleChainOutputStrategy);
}
@Override
public boolean hasRefreshContextOnlyChanges(OutputStrategy other) {
return false;
}
}

21
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java

@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.AttributeScope;
import java.util.Objects;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
@ -50,4 +52,23 @@ public interface Output {
void setDecimalsByDefault(Integer decimalsByDefault);
default boolean hasContextOnlyChanges(Output other) {
if (!getType().equals(other.getType())) {
return true;
}
if (!Objects.equals(getName(), other.getName())) {
return true;
}
if (getScope() != (other.getScope())) {
return true;
}
if (!Objects.equals(getDecimalsByDefault(), other.getDecimalsByDefault())) {
return true;
}
if (getStrategy().hasContextOnlyChanges(other.getStrategy())) {
return true;
}
return false;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java

@ -22,4 +22,8 @@ public interface OutputStrategy {
@JsonIgnore
OutputStrategyType getType();
boolean hasContextOnlyChanges(OutputStrategy other);
boolean hasRefreshContextOnlyChanges(OutputStrategy other);
}

21
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java

@ -35,4 +35,25 @@ public class TimeSeriesImmediateOutputStrategy implements TimeSeriesOutputStrate
public OutputStrategyType getType() {
return OutputStrategyType.IMMEDIATE;
}
@Override
public boolean hasContextOnlyChanges(OutputStrategy other) {
if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) {
return true;
}
boolean saveTimeSeriesUpdated = saveTimeSeries != otherStrategy.isSaveTimeSeries();
boolean saveLatestUpdated = saveLatest != otherStrategy.isSaveLatest();
boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate();
boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs();
return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated;
}
@Override
public boolean hasRefreshContextOnlyChanges(OutputStrategy other) {
if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) {
return true;
}
return ttl != otherStrategy.getTtl();
}
}

11
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java

@ -26,4 +26,15 @@ public class TimeSeriesRuleChainOutputStrategy implements TimeSeriesOutputStrate
public OutputStrategyType getType() {
return OutputStrategyType.RULE_CHAIN;
}
@Override
public boolean hasContextOnlyChanges(OutputStrategy other) {
return !(other instanceof TimeSeriesRuleChainOutputStrategy);
}
@Override
public boolean hasRefreshContextOnlyChanges(OutputStrategy other) {
return false;
}
}

202
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java

@ -0,0 +1,202 @@
/**
* Copyright © 2016-2025 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.common.data.cf.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.AttributeScope;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class)
public class CalculatedFieldOutputTest {
@Test
public void testHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() {
TimeSeriesOutput output = new TimeSeriesOutput();
AttributesOutput newOutput = new AttributesOutput();
assertThat(output.hasContextOnlyChanges(newOutput)).isTrue();
}
@Test
public void testHasContextOnlyChanges_whenNameChanged_shouldReturnTrue() {
TimeSeriesOutput output = new TimeSeriesOutput();
TimeSeriesOutput newOutput = new TimeSeriesOutput();
newOutput.setName("new");
assertThat(output.hasContextOnlyChanges(newOutput)).isTrue();
}
@Test
public void testHasContextOnlyChanges_whenScopeChanged_shouldReturnTrue() {
AttributesOutput output = new AttributesOutput();
output.setScope(AttributeScope.SHARED_SCOPE);
AttributesOutput newOutput = new AttributesOutput();
newOutput.setScope(AttributeScope.SERVER_SCOPE);
assertThat(output.hasContextOnlyChanges(newOutput)).isTrue();
}
@Test
public void testHasContextOnlyChanges_whenDecimalsByDefaultChanged_shouldReturnTrue() {
AttributesOutput output = new AttributesOutput();
AttributesOutput newOutput = new AttributesOutput();
newOutput.setDecimalsByDefault(2);
assertThat(output.hasContextOnlyChanges(newOutput)).isTrue();
}
@Test
public void testHasContextOnlyChanges_whenStrategyHasContextOnlyChanges_shouldReturnTrue() {
AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class);
given(outputStrategy.hasContextOnlyChanges(any())).willReturn(true);
AttributesOutput output = new AttributesOutput();
output.setStrategy(outputStrategy);
AttributesOutput newOutput = new AttributesOutput();
assertThat(output.hasContextOnlyChanges(newOutput)).isTrue();
}
@Test
public void testHasContextOnlyChanges_whenStrategyDoesNotHaveContextOnlyChanges_shouldReturnTrue() {
AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class);
given(outputStrategy.hasContextOnlyChanges(any())).willReturn(false);
AttributesOutput output = new AttributesOutput();
output.setStrategy(outputStrategy);
AttributesOutput newOutput = new AttributesOutput();
assertThat(output.hasContextOnlyChanges(newOutput)).isFalse();
}
/* <strategy>.hasContextOnlyChanges() tests*/
@Test
public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() {
AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true);
AttributesRuleChainOutputStrategy newStrategy = new AttributesRuleChainOutputStrategy();
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSaveAttributesChanged_shouldReturnTrue() {
AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true);
AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() {
AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, true);
AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() {
AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, false);
AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testAttributesRuleChainOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() {
AttributesRuleChainOutputStrategy strategy = new AttributesRuleChainOutputStrategy();
AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, false, true, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() {
TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true);
TimeSeriesRuleChainOutputStrategy newStrategy = new TimeSeriesRuleChainOutputStrategy();
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSaveLatestChanged_shouldReturnTrue() {
TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, false, true, true);
TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() {
TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, false, true);
TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() {
TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false);
TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testTimeSeriesRuleChainOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() {
TimeSeriesRuleChainOutputStrategy strategy = new TimeSeriesRuleChainOutputStrategy();
TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue();
}
/* <strategy>.hasRefreshContextOnlyChanges() tests*/
@Test
public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenUpdateAttrOnValueChangedChanged_shouldReturnTrue() {
AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, false, true, false, true);
AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse();
assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenSendAttrUpdatedNotificationChanged_shouldReturnTrue() {
AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(false, true, true, false, true);
AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse();
assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue();
}
@Test
public void testTimeSeriesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenTtlChanged_shouldReturnTrue() {
TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true);
TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(300, true, true, true, true);
assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse();
assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue();
}
}

2
common/queue/pom.xml

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

2
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java

@ -25,7 +25,7 @@ import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyDictionaryCompositeKey implements Serializable{
public class KeyDictionaryCompositeKey implements Serializable {
@Transient
private static final long serialVersionUID = -4089175869616037523L;

7
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java

@ -36,8 +36,7 @@ public final class KeyDictionaryEntry {
@Column(name = KEY_COLUMN)
private String key;
@Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int")
@Generated
private int keyId;
@Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int", insertable = false, updatable = false)
private Integer keyId;
}
}

68
dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java

@ -17,8 +17,6 @@ package org.thingsboard.server.dao.sqlts.dictionary;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -48,43 +46,34 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override
public Integer getOrSaveKeyId(String strKey) {
Integer keyId = keyDictionaryMap.get(strKey);
if (keyId == null) {
Optional<KeyDictionaryEntry> tsKvDictionaryOptional;
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey));
if (tsKvDictionaryOptional.isEmpty()) {
creationLock.lock();
try {
keyId = keyDictionaryMap.get(strKey);
if (keyId != null) {
return keyId;
}
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey));
if (tsKvDictionaryOptional.isEmpty()) {
KeyDictionaryEntry keyDictionaryEntry = new KeyDictionaryEntry();
keyDictionaryEntry.setKey(strKey);
try {
KeyDictionaryEntry saved = keyDictionaryRepository.save(keyDictionaryEntry);
keyDictionaryMap.put(saved.getKey(), saved.getKeyId());
keyId = saved.getKeyId();
} catch (DataIntegrityViolationException | ConstraintViolationException e) {
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey));
KeyDictionaryEntry dictionary = tsKvDictionaryOptional.orElseThrow(() -> new RuntimeException("Failed to get KeyDictionaryEntry entity from DB!"));
keyDictionaryMap.put(dictionary.getKey(), dictionary.getKeyId());
keyId = dictionary.getKeyId();
}
} else {
keyId = tsKvDictionaryOptional.get().getKeyId();
}
} finally {
creationLock.unlock();
}
} else {
keyId = tsKvDictionaryOptional.get().getKeyId();
keyDictionaryMap.put(strKey, keyId);
Integer cached = keyDictionaryMap.get(strKey);
if (cached != null) {
return cached;
}
var compositeKey = new KeyDictionaryCompositeKey(strKey);
Optional<Integer> existingId = keyDictionaryRepository.findById(compositeKey).map(KeyDictionaryEntry::getKeyId);
if (existingId.isPresent()) {
return cacheAndReturn(strKey, existingId.get());
}
creationLock.lock();
try {
Integer fromCache = keyDictionaryMap.get(strKey);
if (fromCache != null) {
return fromCache;
}
Integer keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey);
if (keyId != null) {
return cacheAndReturn(strKey, keyId);
}
log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey);
keyId = keyDictionaryRepository.findById(compositeKey)
.map(KeyDictionaryEntry::getKeyId)
.orElseThrow(() -> new IllegalStateException(
"Failed to resolve keyId for string key: " + strKey + " after fallback."));
return cacheAndReturn(strKey, keyId);
} finally {
creationLock.unlock();
}
return keyId;
}
@Override
@ -98,4 +87,9 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService
return DaoUtil.pageToPageData(keyDictionaryRepository.findAll(DaoUtil.toPageable(pageLink)));
}
private Integer cacheAndReturn(String key, Integer keyId) {
keyDictionaryMap.put(key, keyId);
return keyId;
}
}

6
dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java

@ -19,6 +19,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
@ -31,4 +32,7 @@ public interface KeyDictionaryRepository extends JpaRepository<KeyDictionaryEntr
@Query("SELECT e FROM KeyDictionaryEntry e ORDER BY e.keyId ASC")
Page<KeyDictionaryEntry> findAll(Pageable pageable);
}
@Query(value = "INSERT INTO key_dictionary (key) VALUES (:key) ON CONFLICT (key) DO UPDATE SET key = EXCLUDED.key RETURNING key_id", nativeQuery = true)
Integer upsertAndGetKeyId(@Param("key") String key);
}

111
dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java

@ -0,0 +1,111 @@
/**
* Copyright © 2016-2025 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.dao.sqlts.dictionary;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.dao.dictionary.KeyDictionaryDao;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@DaoSqlTest
public class KeyDictionaryDaoTest extends AbstractServiceTest {
@Autowired
private KeyDictionaryDao keyDictionaryDao;
@Autowired
private KeyDictionaryRepository keyDictionaryRepository;
private static final String KEY = "testKeyDictionaryDaoTestKey";
@Test
public void testGetOrSaveKeyId_concurrent() throws Exception {
int threads = 8;
ExecutorService executor = Executors.newFixedThreadPool(threads);
CountDownLatch allReady = new CountDownLatch(threads);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch allDone = new CountDownLatch(threads);
Integer[] keyIds = new Integer[threads];
try {
for (int i = 0; i < threads; i++) {
final int idx = i;
executor.submit(() -> {
allReady.countDown();
try {
// wait until all threads are ready
start.await();
// concurrent call
Integer id = keyDictionaryDao.getOrSaveKeyId(KEY);
keyIds[idx] = id;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
allDone.countDown();
}
});
}
// ensure all threads are queued
allReady.await(5, TimeUnit.SECONDS);
// fire the start gun
start.countDown();
// wait for all to finish
allDone.await(10, TimeUnit.SECONDS);
} finally {
executor.shutdownNow();
}
// basic sanity
for (int i = 0; i < threads; i++) {
assertThat(keyIds[i])
.as("keyId[%s]", i)
.isNotNull()
.isGreaterThan(0);
}
// all threads must see the same keyId
int first = keyIds[0];
assertThat(first).isGreaterThan(0);
assertThat(Arrays.stream(keyIds).distinct().count())
.as("all threads should get the same keyId")
.isEqualTo(1);
// DB must have exactly one row for this key and the same id
KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(KEY);
Optional<KeyDictionaryEntry> entry = keyDictionaryRepository.findById(id);
assertThat(entry.isPresent()).isTrue();
assertThat(entry.get().getKeyId()).isEqualTo(first);
keyDictionaryRepository.deleteById(id);
}
}

289
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java

@ -15,10 +15,11 @@
*/
package org.thingsboard.server.msa.connectivity;
import com.google.gson.JsonObject;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
@ -30,26 +31,83 @@ import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rest.client.RestClient;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.domain.DomainInfo;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppStatus;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.HasSubject;
import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.TestProperties;
import javax.net.ssl.SSLContext;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.EMAIL;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MICROSOFT_TEAMS;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin;
public class JavaRestClientTest extends AbstractContainerTest {
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
private RestClient restClient;
private Tenant tenant;
private User user;
@BeforeClass
public void beforeClass() throws Exception {
@ -77,11 +135,25 @@ public class JavaRestClientTest extends AbstractContainerTest {
@BeforeMethod
public void setUp() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
restClient.login("sysadmin@thingsboard.org", "sysadmin");
// create tenant and tenant admin
tenant = new Tenant();
tenant.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.randomAlphabetic(5));
tenant = restClient.saveTenant(tenant);
String email = RandomStringUtils.randomAlphabetic(5) + "@gmail.com";
user = restClient.saveUser(defaultTenantAdmin(tenant.getId(), email), false);
restClient.activateUser(user.getId(), "password123", false);
restClient.login(email, "password123");
}
@AfterMethod
public void tearDown() {
restClient.login("sysadmin@thingsboard.org", "sysadmin");
if (tenant != null) {
restClient.deleteTenant(tenant.getId());
}
}
@Test
@ -123,6 +195,219 @@ public class JavaRestClientTest extends AbstractContainerTest {
PageData<AlarmInfo> allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null);
assertThat(allClearedAlarms.getData()).hasSize(0);
}
@Test
public void testTimeSeriesByReadTsKvQueries() {
Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5)));
assertThat(device).isNotNull();
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
for (int i = 0; i < 3; i++) {
JsonObject values = new JsonObject();
values.addProperty("temperature", i + 25);
testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), JacksonUtil.toJsonNode(createPayload().toString()));
}
restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 25, \"humidity\": 60}"));
restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 27, \"humidity\": 59}"));
restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 33, \"humidity\": 62}"));
EntityTypeFilter filter = new EntityTypeFilter();
filter.setEntityType(EntityType.DEVICE);
var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false);
var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"));
EntityDataQuery entityDataQuery = new EntityDataQuery(filter, pageLink, entityFields, null, null);
AvailableEntityKeys availableEntityKeys = restClient.findAvailableEntityKeysByQuery(entityDataQuery, true, true, null);
assertThat(availableEntityKeys).isNotNull();
assertThat(availableEntityKeys.timeseries()).contains("temperature", "humidity");
}
@Test
public void testFindNotifications() {
NotificationTarget notificationTarget = createNotificationTarget(user.getId());
String notificationText1 = "Notification 1";
NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText1, new NotificationDeliveryMethod[]{WEB});
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationTemplate.getId());
String notificationText2 = "Notification 2";
NotificationTemplate notificationTemplate2 = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText2, new NotificationDeliveryMethod[]{WEB});
NotificationRequest notificationRequest2 = submitNotificationRequest(notificationTarget.getId(), notificationTemplate2.getId());
PageData<NotificationRequestInfo> initialRequests = restClient.getNotificationRequests(new PageLink(30));
assertThat(initialRequests.getTotalElements()).isGreaterThanOrEqualTo(2);
NotificationRequestInfo notificationRequestInfo = restClient.getNotificationRequestById(notificationRequest.getId()).get();
assertThat(notificationRequestInfo.getName()).isEqualTo(notificationRequest.getName());
assertThat(notificationRequestInfo.getTemplateName()).isEqualTo(notificationTemplate.getName());
NotificationRequestPreview requestPreview = restClient.getNotificationRequestPreview(notificationRequest, 10);
assertThat(requestPreview.getTotalRecipientsCount()).isEqualTo(1);
assertThat(requestPreview.getRecipientsPreview()).isEqualTo(List.of(user.getEmail()));
PageData<Notification> notifications = restClient.getNotifications(false, WEB, new PageLink(30));
assertThat(notifications.getTotalElements()).isEqualTo(2);
Integer unreadCount = restClient.getUnreadNotificationsCount(WEB);
assertThat(unreadCount).isEqualTo(2);
restClient.markNotificationAsRead(notifications.getData().get(0).getId());
Integer unreadCountAfterRead = restClient.getUnreadNotificationsCount(WEB);
assertThat(unreadCountAfterRead).isEqualTo(1);
restClient.markAllNotificationsAsRead(WEB);
Integer unreadCountAfterAllRead = restClient.getUnreadNotificationsCount(WEB);
assertThat(unreadCountAfterAllRead).isEqualTo(0);
restClient.deleteNotification(notifications.getData().get(0).getId());
notifications = restClient.getNotifications(false, WEB, new PageLink(30));
assertThat(notifications.getTotalElements()).isEqualTo(1);
restClient.deleteNotificationRequest(notificationRequest.getId());
PageData<NotificationRequestInfo> requestsAfterUpdate = restClient.getNotificationRequests(new PageLink(30));
assertThat(requestsAfterUpdate.getTotalElements()).isEqualTo(initialRequests.getTotalElements() - 1);
List<NotificationDeliveryMethod> availableDeliveryMethods = restClient.getAvailableDeliveryMethods();
assertThat(availableDeliveryMethods).contains(WEB, EMAIL, MICROSOFT_TEAMS);
}
@Test
public void testSaveNotificationSettings() {
NotificationSettings settings = new NotificationSettings();
SlackNotificationDeliveryMethodConfig slackConfig = new SlackNotificationDeliveryMethodConfig();
String slackToken = "xoxb-123123123";
slackConfig.setBotToken(slackToken);
settings.setDeliveryMethodsConfigs(Map.of(
NotificationDeliveryMethod.SLACK, slackConfig
));
restClient.saveNotificationSettings(settings);
NotificationSettings savedSettings = restClient.getNotificationSettings().get();
assertThat(savedSettings.getDeliveryMethodsConfigs()).hasSize(1);
assertThat(savedSettings.getDeliveryMethodsConfigs().get(slackConfig.getMethod())).isEqualTo(slackConfig);
// save user notification settings
var entityActionNotificationPref = new UserNotificationSettings.NotificationPref();
entityActionNotificationPref.setEnabled(true);
entityActionNotificationPref.setEnabledDeliveryMethods(Map.of(
NotificationDeliveryMethod.WEB, true,
NotificationDeliveryMethod.SMS, false,
NotificationDeliveryMethod.EMAIL, false
));
UserNotificationSettings userNotificationSettings = new UserNotificationSettings(Map.of(
NotificationType.ENTITY_ACTION, entityActionNotificationPref
));
UserNotificationSettings saved = restClient.saveUserNotificationSettings(userNotificationSettings);
UserNotificationSettings retrieved = restClient.getUserNotificationSettings().get();
assertThat(retrieved).isEqualTo(saved);
}
@Test
public void testSaveDomain() {
restClient.login("sysadmin@thingsboard.org", "sysadmin");
Domain domain = new Domain();
String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase();
domain.setName(prefix + ".test.com");
Domain savedDomain = restClient.saveDomain(domain);
assertThat(savedDomain.getName()).isEqualTo(domain.getName());
PageData<DomainInfo> domainInfos = restClient.getTenantDomainInfos(new PageLink(10, 0 , prefix));
assertThat(domainInfos.getData()).hasSize(1);
}
@Test
public void testSaveMobileApp() {
restClient.login("sysadmin@thingsboard.org", "sysadmin");
MobileApp mobileApp = new MobileApp();
String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase();
mobileApp.setPkgName(prefix + "test.app.apple");
mobileApp.setPlatformType(PlatformType.ANDROID);
mobileApp.setAppSecret(RandomStringUtils.randomAlphabetic(20));
mobileApp.setStatus(MobileAppStatus.DRAFT);
MobileApp savedMobileApp = restClient.saveMobileApp(mobileApp);
assertThat(savedMobileApp.getName()).isEqualTo(mobileApp.getName());
PageData<MobileApp> retrieved = restClient.getTenantMobileApps(new PageLink(10, 0, prefix));
assertThat(retrieved.getData()).hasSize(1);
MobileAppBundle mobileAppBundle = new MobileAppBundle();
String bundlePrefix = RandomStringUtils.randomAlphabetic(5).toLowerCase();
mobileAppBundle.setTitle(bundlePrefix + "Test Bundle");
mobileAppBundle.setAndroidAppId(savedMobileApp.getId());
MobileAppBundle savedMobileAppBundle = restClient.saveMobileBundle(mobileAppBundle);
PageData<MobileAppBundleInfo> bundleInfos = restClient.getTenantMobileBundleInfos(new PageLink(10, 0, bundlePrefix));
assertThat(bundleInfos.getData()).hasSize(1);
}
private NotificationTarget createNotificationTarget(UserId... usersIds) {
UserListFilter filter = new UserListFilter();
filter.setUsersIds(Arrays.stream(usersIds).map(UUIDBased::getId).toList());
NotificationTarget notificationTarget = new NotificationTarget();
notificationTarget.setName(filter.toString() + org.apache.commons.lang3.RandomStringUtils.randomNumeric(5));
PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
targetConfig.setUsersFilter(filter);
notificationTarget.setConfiguration(targetConfig);
return restClient.saveNotificationTarget(notificationTarget);
}
private NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject,
String text, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setName("Notification template: " + RandomStringUtils.randomAlphabetic(5));
notificationTemplate.setNotificationType(notificationType);
NotificationTemplateConfig config = new NotificationTemplateConfig();
config.setDeliveryMethodsTemplates(new HashMap<>());
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate;
switch (deliveryMethod) {
case WEB: {
deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate();
break;
}
case EMAIL: {
deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
break;
}
case SMS: {
deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate();
break;
}
case MOBILE_APP:
deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate();
break;
default:
throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod);
}
deliveryMethodNotificationTemplate.setEnabled(true);
deliveryMethodNotificationTemplate.setBody(text);
if (deliveryMethodNotificationTemplate instanceof HasSubject) {
((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject);
}
config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate);
}
notificationTemplate.setConfiguration(config);
return restClient.saveNotificationTemplate(notificationTemplate);
}
private NotificationRequest submitNotificationRequest(NotificationTargetId targetId, NotificationTemplateId notificationTemplateId) {
NotificationRequestConfig config = new NotificationRequestConfig();
config.setSendingDelayInSec(0);
NotificationRequest notificationRequest = NotificationRequest.builder()
.targets(List.of(targetId).stream().map(UUIDBased::getId).collect(Collectors.toList()))
.templateId(notificationTemplateId)
.additionalConfig(config)
.build();
return restClient.saveNotificationRequest(notificationRequest);
}
}

2
msa/js-executor/package.json

@ -15,7 +15,7 @@
"dependencies": {
"config": "^4.1.1",
"express": "^5.1.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"kafkajs": "^2.2.4",
"long": "^5.3.2",
"uuid-parse": "^1.1.0",

14
msa/js-executor/yarn.lock

@ -945,10 +945,10 @@ isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@ -1533,9 +1533,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92"
integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==
version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930"
integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"

2
msa/web-ui/package.json

@ -19,7 +19,7 @@
"express": "^5.1.0",
"http": "0.0.0",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},

14
msa/web-ui/yarn.lock

@ -1017,10 +1017,10 @@ isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@ -1615,9 +1615,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92"
integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==
version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930"
integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"

14
pom.xml

@ -86,7 +86,7 @@
org.apache.kafka.common.network.NetworkReceive class in the application module. It addresses the issue https://issues.apache.org/jira/browse/KAFKA-4090.
Here is the source to track https://github.com/apache/kafka/tree/trunk/clients/src/main/java/org/apache/kafka/common/network -->
<kafka.version>3.9.1</kafka.version>
<lz4.version>1.8.1</lz4.version> <!-- to fix CVE-2025-12183 introduced through kafka-clients 3.9.1 TODO: remove when kafka-clients is bumped -->
<lz4.version>1.10.1</lz4.version> <!-- to fix CVE-2025-12183 and CVE-2025-66566 introduced through kafka-clients 3.9.1 TODO: remove when kafka-clients is bumped -->
<bucket4j.version>8.10.1</bucket4j.version>
<antlr.version>3.5.3</antlr.version>
<snakeyaml.version>2.2</snakeyaml.version>
@ -102,7 +102,7 @@
<passay.version>1.6.4</passay.version>
<ua-parser.version>1.6.1</ua-parser.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
<commons-beanutils.version>1.11.0</commons-beanutils.version>
<commons-collections.version>4.4</commons-collections.version>
<protobuf-dynamic.version>1.0.4TB</protobuf-dynamic.version>
<wire-schema.version>3.7.1</wire-schema.version>
@ -147,7 +147,7 @@
<firebase-admin.version>9.2.0</firebase-admin.version>
<snappy.version>1.1.10.5</snappy.version>
<rocksdbjni.version>9.10.0</rocksdbjni.version>
<netty.version>4.1.125.Final</netty.version> <!-- to fix CVEs. TODO: remove when fixed in spring-boot-dependencies -->
<netty.version>4.1.128.Final</netty.version> <!-- to fix CVEs. TODO: remove when fixed in spring-boot-dependencies -->
</properties>
<modules>
@ -1162,7 +1162,7 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
<version>${lz4.version}</version> <!-- to fix CVE introduced through kafka-clients 3.9.1 -->
</dependency>
@ -1445,6 +1445,12 @@
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
<version>${cassandra-all.version}</version>
<exclusions>
<exclusion>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testng</groupId>

42
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -147,6 +147,8 @@ import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
@ -2301,7 +2303,8 @@ public class RestClient implements Closeable {
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<DomainInfo>>() {
}).getBody();
},
params).getBody();
}
public Optional<DomainInfo> getDomainInfoById(DomainId domainId) {
@ -2337,7 +2340,8 @@ public class RestClient implements Closeable {
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<MobileApp>>() {
}).getBody();
},
params).getBody();
}
public Optional<MobileApp> getMobileAppById(MobileAppId mobileAppId) {
@ -2369,7 +2373,8 @@ public class RestClient implements Closeable {
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<MobileAppBundleInfo>>() {
}).getBody();
},
params).getBody();
}
public Optional<MobileAppBundle> getMobileBundleById(MobileAppBundleId mobileAppBundleId) {
@ -4334,11 +4339,23 @@ public class RestClient implements Closeable {
}
}
public PageData<Notification> getNotifications(PageLink pageLink) {
public PageData<Notification> getNotifications(Boolean unreadOnly, NotificationDeliveryMethod deliveryMethod, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(baseURL).append("/api/notifications?").append(getUrlParams(pageLink));
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/notifications?" + getUrlParams(pageLink),
if (unreadOnly != null) {
urlBuilder.append("&unreadOnly={unreadOnly}");
params.put("unreadOnly", unreadOnly.toString());
}
if (deliveryMethod != null) {
urlBuilder.append("&deliveryMethod={deliveryMethod}");
params.put("deliveryMethod", deliveryMethod.name());
}
return restTemplate.exchange(urlBuilder.toString(),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<Notification>>() {
@ -4379,7 +4396,8 @@ public class RestClient implements Closeable {
baseURL + uri,
HttpMethod.PUT,
HttpEntity.EMPTY,
Void.class);
Void.class,
params);
}
@ -4387,7 +4405,7 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId());
}
public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) {
public NotificationRequest saveNotificationRequest(NotificationRequest notificationRequest) {
return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody();
}
@ -4466,6 +4484,14 @@ public class RestClient implements Closeable {
}
}
public NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) {
return restTemplate.postForEntity(baseURL + "/api/notification/target", notificationTarget, NotificationTarget.class).getBody();
}
public NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) {
return restTemplate.postForEntity(baseURL + "/api/notification/template", notificationTemplate, NotificationTemplate.class).getBody();
}
public AiModel saveAiModel(AiModel aiModel) {
return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody();
}

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

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

4
tools/pom.xml

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

66
ui-ngx/patches/@angular+common+18.2.13.patch

@ -0,0 +1,66 @@
diff --git a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs
index da69c17..d17f6ad 100755
--- a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs
+++ b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs
@@ -19,6 +19,10 @@ export const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME
providedIn: 'root',
factory: () => XSRF_DEFAULT_HEADER_NAME,
});
+/**
+ * Regex to match absolute URLs, including protocol-relative URLs.
+ */
+const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
/**
* Retrieves the current XSRF token to use with the next outgoing request.
*
@@ -69,7 +73,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
args: [XSRF_COOKIE_NAME]
}] }] });
export function xsrfInterceptorFn(req, next) {
- const lcUrl = req.url.toLowerCase();
// Skip both non-mutating requests and absolute URLs.
// Non-mutating requests don't require a token, and absolute URLs require special handling
// anyway as the cookie set
@@ -77,8 +80,7 @@ export function xsrfInterceptorFn(req, next) {
if (!inject(XSRF_ENABLED) ||
req.method === 'GET' ||
req.method === 'HEAD' ||
- lcUrl.startsWith('http://') ||
- lcUrl.startsWith('https://')) {
+ ABSOLUTE_URL_REGEX.test(req.url)) {
return next(req);
}
const token = inject(HttpXsrfTokenExtractor).getToken();
diff --git a/node_modules/@angular/common/fesm2022/http.mjs b/node_modules/@angular/common/fesm2022/http.mjs
index 1655480..d1dbb38 100755
--- a/node_modules/@angular/common/fesm2022/http.mjs
+++ b/node_modules/@angular/common/fesm2022/http.mjs
@@ -2352,6 +2352,10 @@ const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME' : '',
providedIn: 'root',
factory: () => XSRF_DEFAULT_HEADER_NAME,
});
+/**
+ * Regex to match absolute URLs, including protocol-relative URLs.
+ */
+const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
/**
* Retrieves the current XSRF token to use with the next outgoing request.
*
@@ -2402,7 +2406,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
args: [XSRF_COOKIE_NAME]
}] }] });
function xsrfInterceptorFn(req, next) {
- const lcUrl = req.url.toLowerCase();
// Skip both non-mutating requests and absolute URLs.
// Non-mutating requests don't require a token, and absolute URLs require special handling
// anyway as the cookie set
@@ -2410,8 +2413,7 @@ function xsrfInterceptorFn(req, next) {
if (!inject(XSRF_ENABLED) ||
req.method === 'GET' ||
req.method === 'HEAD' ||
- lcUrl.startsWith('http://') ||
- lcUrl.startsWith('https://')) {
+ ABSOLUTE_URL_REGEX.test(req.url)) {
return next(req);
}
const token = inject(HttpXsrfTokenExtractor).getToken();

88
ui-ngx/patches/@angular+compiler+18.2.13.patch

@ -0,0 +1,88 @@
diff --git a/node_modules/@angular/compiler/fesm2022/compiler.mjs b/node_modules/@angular/compiler/fesm2022/compiler.mjs
index a00b189..260e7be 100755
--- a/node_modules/@angular/compiler/fesm2022/compiler.mjs
+++ b/node_modules/@angular/compiler/fesm2022/compiler.mjs
@@ -18631,6 +18631,7 @@ function SECURITY_SCHEMA() {
'area|ping',
'audio|src',
'a|href',
+ 'a|xlink:href',
'a|ping',
'blockquote|cite',
'body|background',
@@ -18644,6 +18645,75 @@ function SECURITY_SCHEMA() {
'track|src',
'video|poster',
'video|src',
+
+ // MathML namespace
+ // https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1
+ 'annotation|href',
+ 'annotation|xlink:href',
+ 'annotation-xml|href',
+ 'annotation-xml|xlink:href',
+ 'maction|href',
+ 'maction|xlink:href',
+ 'malignmark|href',
+ 'malignmark|xlink:href',
+ 'math|href',
+ 'math|xlink:href',
+ 'mroot|href',
+ 'mroot|xlink:href',
+ 'msqrt|href',
+ 'msqrt|xlink:href',
+ 'merror|href',
+ 'merror|xlink:href',
+ 'mfrac|href',
+ 'mfrac|xlink:href',
+ 'mglyph|href',
+ 'mglyph|xlink:href',
+ 'msub|href',
+ 'msub|xlink:href',
+ 'msup|href',
+ 'msup|xlink:href',
+ 'msubsup|href',
+ 'msubsup|xlink:href',
+ 'mmultiscripts|href',
+ 'mmultiscripts|xlink:href',
+ 'mprescripts|href',
+ 'mprescripts|xlink:href',
+ 'mi|href',
+ 'mi|xlink:href',
+ 'mn|href',
+ 'mn|xlink:href',
+ 'mo|href',
+ 'mo|xlink:href',
+ 'mpadded|href',
+ 'mpadded|xlink:href',
+ 'mphantom|href',
+ 'mphantom|xlink:href',
+ 'mrow|href',
+ 'mrow|xlink:href',
+ 'ms|href',
+ 'ms|xlink:href',
+ 'mspace|href',
+ 'mspace|xlink:href',
+ 'mstyle|href',
+ 'mstyle|xlink:href',
+ 'mtable|href',
+ 'mtable|xlink:href',
+ 'mtd|href',
+ 'mtd|xlink:href',
+ 'mtr|href',
+ 'mtr|xlink:href',
+ 'mtext|href',
+ 'mtext|xlink:href',
+ 'mover|href',
+ 'mover|xlink:href',
+ 'munder|href',
+ 'munder|xlink:href',
+ 'munderover|href',
+ 'munderover|xlink:href',
+ 'semantics|href',
+ 'semantics|xlink:href',
+ 'none|href',
+ 'none|xlink:href',
]);
registerContext(SecurityContext.RESOURCE_URL, [
'applet|code',

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

@ -84,7 +84,7 @@
</div>
<ng-container formGroupName="configuration">
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<div class="tb-form-panel-title tb-required" [class.tb-disabled]="!fieldFormGroup.get('entityId.id').value || !fieldFormGroup.get('name').value">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="data.entityId || fieldFormGroup.get('entityId').value"
[tenantId]="data.tenantId"
@ -94,14 +94,14 @@
[entityName]="entityName"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div>
<div class="tb-form-panel-title" [class.tb-disabled]="!disabledClearRuleButton">{{ 'alarm-rule.create-conditions' | translate }}</div>
<div class="flex flex-1 flex-col">
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments" [testScript]="onTestScript.bind(this)">
</tb-create-cf-alarm-rules>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.clear-condition' | translate }}</div>
<div class="tb-form-panel-title" [class.tb-disabled]="!disabledClearRuleButton">{{ 'alarm-rule.clear-condition' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2 pb-2"
[class.!hidden]="!configFormGroup.get('clearRule').value">
<div class="clear-alarm-rule flex flex-1 flex-row">
@ -118,7 +118,7 @@
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
<span translate class="tb-prompt text-base flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
</div>
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
@ -131,7 +131,7 @@
</div>
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="false">
<mat-expansion-panel-header>
<mat-expansion-panel-header [class.tb-disabled]="!disabledClearRuleButton">
{{ 'alarm-rule.advanced-settings' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>

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

@ -17,6 +17,10 @@
.calculated-field-dialog-container {
width: 869px;
max-width: 100%;
.tb-disabled {
color: rgba(0, 0, 0, 0.38);
}
}
.clear-alarm-rule {

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

@ -75,7 +75,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
id: [null as null | string, Validators.required],
}),
configuration: this.fb.group({
arguments: this.fb.control({}),
arguments: this.fb.control({}, Validators.required),
propagate: [false],
propagateToOwner: [false],
propagateToTenant: [false],
@ -164,30 +164,6 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
this.configFormGroup.patchValue({clearRule: clearAlarmRule});
}
removeRelationType(key: string): void {
const keys: string[] = this.configFormGroup.get('propagateRelationTypes').value;
const index = keys.indexOf(key);
if (index >= 0) {
keys.splice(index, 1);
this.configFormGroup.get('propagateRelationTypes').setValue(keys);
}
}
addRelationType(event: MatChipInputEvent): void {
const input = event.chipInput.inputElement;
let value = (event.value ?? '').trim();
if (value) {
let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? [];
if (keys.indexOf(value) === -1) {
keys.push(value);
this.configFormGroup.get('propagateRelationTypes').setValue(keys);
}
}
if (input) {
input.value = '';
}
}
get fromGroupValue(): CalculatedField {
return deepTrim(this.fieldFormGroup.value as CalculatedField);
}
@ -197,7 +173,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
}
add(): void {
if (this.fieldFormGroup.valid) {
if (this.fieldFormGroup.valid && Object.keys(this.arguments ?? {}).length > 0) {
const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue};
alarmRule.configuration.type = CalculatedFieldType.ALARM;
@ -206,8 +182,8 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
.subscribe(calculatedField => this.dialogRef.close(calculatedField));
} else {
this.fieldFormGroup.get('name').markAsTouched();
this.entityTypeSelect.markAsTouched();
this.entityAutocompleteComponent.markAsTouched();
this.entityTypeSelect?.markAsTouched();
this.entityAutocompleteComponent?.markAsTouched();
}
}
@ -235,10 +211,18 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
if (Object.keys(this.arguments ?? {}).length > 0) {
this.fieldFormGroup.get('configuration.createRules').enable({emitEvent: false});
this.fieldFormGroup.get('configuration.clearRule').enable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagate').enable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagateToOwner').enable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagateToTenant').enable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagateRelationTypes').enable({emitEvent: false});
this.disabledClearRuleButton = true;
} else {
this.fieldFormGroup.get('configuration.createRules').disable({emitEvent: false});
this.fieldFormGroup.get('configuration.clearRule').disable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagate').disable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagateToOwner').disable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagateToTenant').disable({emitEvent: false});
this.fieldFormGroup.get('configuration.propagateRelationTypes').disable({emitEvent: false});
this.disabledClearRuleButton = false;
}
}

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

@ -55,14 +55,12 @@ import {
AlarmRuleDialogComponent,
AlarmRuleDialogData
} from "@home/components/alarm-rules/alarm-rule-dialog.component";
import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldDebugDialogData
} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component";
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
import { UtilsService } from "@core/services/utils.service";
import { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component';
import { DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models';
import { ActionNotificationShow } from "@core/notification/notification.actions";
import {
CalculatedFieldScriptTestDialogComponent,
@ -266,13 +264,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
}
private openDebugEventsDialog(calculatedField: CalculatedField): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
this.dialog.open<EventsDialogComponent, EventsDialogData, null>(EventsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
title: 'alarm-rule.debugging',
tenantId: this.tenantId,
value: calculatedField,
getTestScriptDialogFn: null,
entityId: calculatedField.id,
debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD],
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
debugActionEnabledFn: () => false
}
})
.afterClosed()

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

@ -19,12 +19,6 @@
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'alarm-rule.alarm-rule-condition' : 'alarm-rule.edit-alarm-rule-condition') | translate }}</h2>
<span class="flex-1"></span>
<div formGroupName="expression" class="flex flex-row items-center justify-start gt-xs:gap-4" style="min-width: fit-content;">
<tb-toggle-select formControlName="type" appearance="fill-invert" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="AlarmRuleExpressionType.SIMPLE">{{ 'alarm-rule.expression-type.simple' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="AlarmRuleExpressionType.TBEL">{{ 'alarm-rule.expression-type.script' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -33,105 +27,116 @@
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-padding no-border">
<div class="flex flex-1 flex-col" formGroupName="expression">
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-alarm-rule-filter-list
[operation]="conditionFormGroup.get('expression.operation').value"
[arguments]="arguments"
formControlName="filters">
</tb-alarm-rule-filter-list>
</div>
} @else {
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">
{{ 'alarm-rule.script' | translate }}
</div>
<tb-js-func formControlName="expression"
functionName="expression"
class="tb-alarm-rule-script"
[disableUndefinedCheck]="true"
[scriptLanguage]="scriptLanguage.TBEL"
[functionArgs]="functionArgs"
[highlightRules]="argumentsHighlightRules"
[editorCompleter]="argumentsEditorCompleter"
noValidate="true"
[helpPopupStyle]="{ width: '1200px' }"
helpId="alarm-rule/expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.tbel' | translate }}
<div class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between" formGroupName="expression">
<div class="tb-form-panel-title">{{ 'alarm-rule.mode' | translate }}</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="AlarmRuleExpressionType.SIMPLE">{{ 'alarm-rule.expression-type.simple' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="AlarmRuleExpressionType.TBEL">{{ 'alarm-rule.expression-type.script' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-padding no-border">
<div class="flex flex-1 flex-col" formGroupName="expression">
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-alarm-rule-filter-list
[operation]="conditionFormGroup.get('expression.operation').value"
[arguments]="arguments"
formControlName="filters">
</tb-alarm-rule-filter-list>
</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!argumentsList.length"
(click)="onTestScript($event)">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript($event)"
[disabled]="!argumentsList.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
} @else {
<div class="tb-form-panel no-gap stroked">
<div class="tb-form-panel-title tb-required">
{{ 'alarm-rule.script' | translate }}
</div>
<tb-js-func formControlName="expression"
functionName="expression"
class="tb-alarm-rule-script"
[disableUndefinedCheck]="true"
[scriptLanguage]="scriptLanguage.TBEL"
[functionArgs]="functionArgs"
[highlightRules]="argumentsHighlightRules"
[editorCompleter]="argumentsEditorCompleter"
noValidate="true"
[helpPopupStyle]="{ width: '1200px' }"
helpId="alarm-rule/expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.tbel' | translate }}
</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!argumentsList.length"
(click)="onTestScript($event)">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript($event)"
[disabled]="!argumentsList.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
</div>
}
</div>
}
</div>
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="true">
<mat-expansion-panel-header>
{{ 'alarm-rule.filter-preview' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div>
@if (specText) {
<span class="spec-text">{{ specText }}</span>
}
@if (conditionFormGroup.get('expression.filters').value?.length) {
<tb-alarm-rule-filter-text [alarmRuleExpression]="conditionFormGroup.get('expression').value"
[arguments]="arguments"
class="flex-1"
[nowrap]="false"
required>
</tb-alarm-rule-filter-text>
} @else {
<span class="tb-prompt justify-center">{{ 'alarm-rule.no-filter-preview' | translate }}</span>
}
</div>
</ng-template>
</mat-expansion-panel>
</div>
}
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel no-gap stroked">
<mat-expansion-panel class="tb-settings" [expanded]="true">
<mat-expansion-panel-header>
{{ 'alarm-rule.filter-preview' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div>
@if (specText) {
<span class="spec-text">{{ specText }}</span>
}
@if (conditionFormGroup.get('expression.filters').value?.length) {
<tb-alarm-rule-filter-text [alarmRuleExpression]="conditionFormGroup.get('expression').value"
[arguments]="arguments"
class="flex-1"
[nowrap]="false"
required>
</tb-alarm-rule-filter-text>
} @else {
<span class="tb-prompt text-base justify-center">{{ 'alarm-rule.no-filter-preview' | translate }}</span>
}
</div>
</ng-template>
</mat-expansion-panel>
</div>
}
</div>
</div>
<section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"
[disabled]="isNoData ? alarmConditionType === AlarmConditionType.REPEATING || alarmConditionType === AlarmConditionType.DURATION : false">
<section class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.type' | translate }}</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" [disabled]="isNoData">
<tb-toggle-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
@if (isNoData) {
<mat-hint translate>alarm-rule.condition-type-hint</mat-hint>
}
</mat-form-field>
</tb-toggle-option>
</tb-toggle-select>
</div>
@if (isNoData) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<span>{{ 'alarm-rule.condition-type-hint' | translate }}</span>
</div>
}
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
<div class="tb-form-panel stroked no-padding-bottom">
<div class="flex flex-row items-center justify-between">

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

@ -31,7 +31,7 @@
[nowrap]="true"
[specText]="specText"
required
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
addFilterPrompt="{{ 'alarm-rule.enter-alarm-rule-condition-prompt' | translate }}">
</tb-alarm-rule-filter-text>
<mat-icon [color]="disabled ? '' : (conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn')"
class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">

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

@ -19,10 +19,6 @@
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}</h2>
<span class="flex-1"></span>
<tb-toggle-select [formControl]="dynamicModeControl" appearance="fill-invert" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="false">{{ 'alarm-rule.static-schedule' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic-schedule' | translate }}</tb-toggle-option>
</tb-toggle-select>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -30,10 +26,21 @@
</button>
</mat-toolbar>
<div mat-dialog-content>
<tb-cf-alarm-schedule [arguments]="arguments"
[dynamicMode]="dynamicModeControl.value"
[formControl]="alarmScheduleControl">
</tb-cf-alarm-schedule>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel stroked no-padding-bottom">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.mode' | translate }}</div>
<tb-toggle-select [formControl]="dynamicModeControl" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="false">{{ 'alarm-rule.static-schedule' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic-schedule' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-cf-alarm-schedule [arguments]="arguments"
[dynamicMode]="dynamicModeControl.value"
[formControl]="alarmScheduleControl">
</tb-cf-alarm-schedule>
</div>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"

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

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

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

@ -25,6 +25,7 @@
max-height: 300px;
.no-data-found {
height: 50px;
font-size: 16px;
}
&-divider {

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

@ -22,6 +22,7 @@
.predicate-list {
.no-data-found {
height: 50px;
font-size: 16px;
}
}

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

@ -150,6 +150,13 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
this.filterPredicateFormGroup.disable({emitEvent: false});
} else {
this.filterPredicateFormGroup.enable({emitEvent: false});
if (this.filterPredicateFormGroup.get('operation').value === 'NO_DATA') {
this.filterPredicateFormGroup.get('duration').enable({emitEvent: false});
this.filterPredicateFormGroup.get('value').disable({emitEvent: false});
} else {
this.filterPredicateFormGroup.get('duration').disable({emitEvent: false});
this.filterPredicateFormGroup.get('value').enable({emitEvent: false});
}
}
}
@ -170,12 +177,8 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
}
if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) {
this.type = AlarmRuleFilterPredicateType[this.valueType];
this.filterPredicateFormGroup.get('duration').enable({emitEvent: false});
this.filterPredicateFormGroup.get('value').disable({emitEvent: false});
this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false});
} else {
this.filterPredicateFormGroup.get('duration').disable({emitEvent: false});
this.filterPredicateFormGroup.get('value').enable({emitEvent: false});
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
}
}

29
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -22,7 +22,7 @@ import {
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link';
import { EMPTY, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
@ -48,8 +48,6 @@ import {
PropagationWithExpression,
} from '@shared/models/calculated-field.models';
import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldDebugDialogData,
CalculatedFieldDialogComponent,
CalculatedFieldDialogData,
CalculatedFieldScriptTestDialogComponent,
@ -61,6 +59,8 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity
import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions";
import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models';
import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
@ -233,13 +233,30 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}
private openDebugEventsDialog(calculatedField: CalculatedField): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
const debugActionEnabledFn = (event: DebugEvent) => {
return (calculatedField.type === CalculatedFieldType.SCRIPT ||
(calculatedField.type === CalculatedFieldType.PROPAGATION &&
calculatedField.configuration.applyExpressionToResolvedArguments)
) && !!(event as DebugEvent).body.arguments;
};
const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef<EventsDialogComponent, string>) => {
this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments))
.subscribe(expression => dialogRef.close(expression));
};
this.dialog.open<EventsDialogComponent, EventsDialogData, null>(EventsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
title: 'calculated-fields.debugging',
tenantId: this.tenantId,
value: calculatedField,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
entityId: calculatedField.id,
debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD],
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
onDebugEventSelected,
debugActionEnabledFn
}
})
.afterClosed()

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

@ -318,6 +318,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
).subscribe((key) => {
if (this.argumentFormGroup.get('argumentName').pristine) {
this.argumentFormGroup.get('argumentName').setValue(key);
this.argumentFormGroup.get('argumentName').markAsTouched({emitEvent: false});
}
});
}

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

@ -128,7 +128,7 @@
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
[class.required]="!disable"
class="tb-prompt flex flex-1 items-end justify-center tb-prompt required">
class="tb-prompt flex flex-1 items-end justify-center text-base required">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
<div class="flex h-9 justify-between">

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

@ -41,6 +41,10 @@
}
}
.no-data-found {
font-size: 16px;
}
.max-args-warning {
.mat-icon {
color: #FAA405;

77
ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts

@ -1,77 +0,0 @@
///
/// Copyright © 2016-2025 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.
///
import { AfterViewInit, Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models';
import { EventTableComponent } from '@home/components/event/event-table.component';
import {
CalculatedField,
CalculatedFieldTestScriptFn,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
export interface CalculatedFieldDebugDialogData {
tenantId: string;
value: CalculatedField;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
}
@Component({
selector: 'tb-calculated-field-debug-dialog',
styleUrls: ['calculated-field-debug-dialog.component.scss'],
templateUrl: './calculated-field-debug-dialog.component.html',
})
export class CalculatedFieldDebugDialogComponent extends DialogComponent<CalculatedFieldDebugDialogComponent, string> implements AfterViewInit {
@ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent;
readonly DebugEventType = DebugEventType;
readonly debugEventTypes = DebugEventType;
readonly EventType = EventType;
dialogTitle: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, string>) {
super(store, router, dialogRef);
this.dialogTitle = this.data.value.type === CalculatedFieldType.ALARM ? 'alarm-rule.debugging' : 'calculated-fields.debugging';
}
ngAfterViewInit(): void {
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => {
return (this.data.value.type === CalculatedFieldType.SCRIPT ||
(this.data.value.type === CalculatedFieldType.PROPAGATION && this.data.value.configuration.applyExpressionToResolvedArguments)
) && !!(event as Event).body.arguments
});
this.eventsTable.entitiesTable.updateData();
}
cancel(): void {
this.dialogRef.close(null);
}
onDebugEventSelected(event: CalculatedFieldEventBody): void {
this.data.getTestScriptDialogFn(this.data.value, JSON.parse(event.arguments))
.subscribe(expression => this.dialogRef.close(expression));
}
}

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

@ -278,7 +278,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner;
this.geofencingFormGroup.get('refEntityId').get('id').setValue(null);
this.geofencingFormGroup.get('perimeterKeyName').reset('');
const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type);
const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner, ArgumentEntityType.RelationQuery].includes(type);
this.geofencingFormGroup.get('refEntityId').get('id')[isEntityWithId ? 'enable' : 'disable']();
if (!isEntityWithId) {
this.entityNameSubject.next(null);

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

@ -128,7 +128,7 @@
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center tb-prompt required">
class="tb-prompt flex flex-1 items-end justify-center text-base required">
{{ 'calculated-fields.no-zone-configured' | translate }}
</div>
<div class="flex h-9 justify-between">

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

@ -102,7 +102,7 @@
</table>
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center required">
class="tb-prompt flex flex-1 items-end justify-center text-base required">
{{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
</div>
<div class="flex h-9 justify-between">

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts

@ -15,5 +15,4 @@
///
export * from './dialog/calculated-field-dialog.component';
export * from './debug-dialog/calculated-field-debug-dialog.component';
export * from './test-dialog/calculated-field-script-test-dialog.component';

9
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -192,9 +192,6 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo
import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component";
import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component";
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
import {
CalculatedFieldDebugDialogComponent
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
import { AlarmRuleModule } from "@home/components/alarm-rules/alarm-rule.module";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component";
@ -204,6 +201,7 @@ import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/ed
import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component';
import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component';
import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component";
import { EventsDialogComponent } from '@home/dialogs/events-dialog.component';
@NgModule({
declarations:
@ -217,7 +215,6 @@ import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-fi
AuditLogTableComponent,
AuditLogDetailsDialogComponent,
CalculatedFieldsTableComponent,
CalculatedFieldDebugDialogComponent,
AlarmRulesTableComponent,
EventContentDialogComponent,
EventTableHeaderComponent,
@ -361,7 +358,8 @@ import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-fi
EditApiKeyDescriptionPanelComponent,
ApiKeyGeneratedDialogComponent,
AuditLogHeaderComponent,
AuditLogFilterComponent
AuditLogFilterComponent,
EventsDialogComponent
],
imports: [
CommonModule,
@ -510,6 +508,7 @@ import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-fi
ResourcesLibraryComponent,
ApiKeysTableComponent,
ApiKeysTableDialogComponent,
EventsDialogComponent
],
providers: [
WidgetComponentService,

15
ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html → ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html

@ -17,7 +17,7 @@
-->
<div class="debug-dialog-container flex flex-col xs:h-full">
<mat-toolbar color="primary">
<h2>{{ dialogTitle | translate}}</h2>
<h2>{{ data.title | translate}}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
@ -27,13 +27,14 @@
</mat-toolbar>
<div mat-dialog-content class="tb-form-panel stroked debug-dialog-content xs:flex-1">
<tb-event-table
[tenantId]="data.tenantId"
[debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[debugEventTypes]="data.debugEventTypes"
[defaultEventType]="data.defaultEventType"
[disabledEventTypes]="data.disabledEventTypes ?? []"
[active]="true"
[entityId]="data.value.id"
(debugEventSelected)="onDebugEventSelected($event)"
[tenantId]="data.tenantId"
[entityId]="data.entityId"
[functionTestButtonLabel]="data.functionTestButtonLabel ?? ''"
(debugEventSelected)="this.onDebugEventSelected($event)"
/>
</div>
<div mat-dialog-actions class="justify-end">

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss → ui-ngx/src/app/modules/home/dialogs/events-dialog.component.scss

72
ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts

@ -0,0 +1,72 @@
///
/// Copyright © 2016-2025 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.
///
import { AfterViewInit, Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DialogComponent } from '@shared/components/dialog.component';
import { EventTableComponent } from '@home/components/event/event-table.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DebugEventType, EventType } from '@shared/models/event.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData, HasId } from '@shared/models/base-data';
export interface EventsDialogData {
title: string;
tenantId: string;
entityId: EntityId;
debugEventTypes: Array<DebugEventType>;
defaultEventType: DebugEventType;
disabledEventTypes?: Array<EventType | DebugEventType>;
functionTestButtonLabel?: string;
onDebugEventSelected?: (event: any, dialogRef: MatDialogRef<EventsDialogComponent, string>) => void;
debugActionEnabledFn?: (event: BaseData<HasId>) => boolean;
}
@Component({
selector: 'tb-debug-dialog',
templateUrl: './events-dialog.component.html',
styleUrl: './events-dialog.component.scss'
})
export class EventsDialogComponent extends DialogComponent<EventsDialogComponent, string> implements AfterViewInit{
@ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EventsDialogData,
protected dialogRef: MatDialogRef<EventsDialogComponent, string>) {
super(store, router, dialogRef);
}
ngAfterViewInit() {
if (this.data.debugActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) {
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.debugActionEnabledFn;
}
this.eventsTable.entitiesTable.updateData();
}
cancel(): void {
this.dialogRef.close(null);
}
onDebugEventSelected(event: any): void {
if(this.data.onDebugEventSelected) {
this.data.onDebugEventSelected(event, this.dialogRef);
}
}
}

1
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -36,6 +36,7 @@
</mat-form-field>
<section class="mb-5 flex max-w-xs flex-row">
<tb-entity-debug-settings-button
[additionalActionConfig]="additionalActionConfig"
class="mr-2"
formControlName="debugSettings"
[entityType]="entityType.RULE_NODE"

4
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts

@ -37,6 +37,7 @@ import { ComponentClusteringMode } from '@shared/models/component-descriptor.mod
import { coerceBoolean } from '@shared/decorators/coercion';
import { ServiceType } from '@shared/models/queue.models';
import { takeUntil } from 'rxjs/operators';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
@Component({
selector: 'tb-rule-node',
@ -64,6 +65,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
@coerceBoolean()
isAdd = false;
@Input()
additionalActionConfig: AdditionalDebugActionConfig;
@Output()
initRuleNode = new EventEmitter<void>();

1
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -106,6 +106,7 @@
<mat-tab-group mat-stretch-tabs="false" class="tb-absolute-fill tb-rulenode-details flex-1" [(selectedIndex)]="selectedRuleNodeTabIndex">
<mat-tab label="{{ 'rulenode.details' | translate }}">
<tb-rule-node #tbRuleNode
[additionalActionConfig]="additionalActionConfig"
[ruleNode]="editingRuleNode"
[ruleChainId]="ruleChain.id?.id"
[ruleChainType]="ruleChainType"

37
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -99,6 +99,8 @@ import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import { EntityDebugSettings } from '@shared/models/entity.models';
import Timeout = NodeJS.Timeout;
import { DomSanitizer } from '@angular/platform-browser';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { EventsDialogComponent } from '@home/dialogs/events-dialog.component';
@Component({
selector: 'tb-rulechain-page',
@ -1366,7 +1368,7 @@ export class RuleChainPageComponent extends PageComponent
details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description);
}
}
name = this.sanitizer.sanitize(SecurityContext.HTML, name);
desc = this.sanitizer.sanitize(SecurityContext.HTML, desc);
@ -1656,6 +1658,39 @@ export class RuleChainPageComponent extends PageComponent
}
}
get additionalActionConfig (): AdditionalDebugActionConfig {
return {
title: this.translate.instant('rulenode.rule-node-events'),
action: this.switchToEventsTab.bind(this)
}
}
private switchToEventsTab() {
if(!this.ruleNodeComponent.ruleNodeFormGroup.dirty) {
this.selectedRuleNodeTabIndex = 1;
} else {
this.openDebugEventsDialog();
}
}
private openDebugEventsDialog(): void {
this.dialog.open<EventsDialogComponent>(EventsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
title: 'rulenode.events',
debugEventTypes: [DebugEventType.DEBUG_RULE_NODE],
defaultEventType: DebugEventType.DEBUG_RULE_NODE,
tenantId: this.ruleChain.tenantId.id,
entityId: this.editingRuleNode.ruleNodeId,
functionTestButtonLabel: this.ruleNodeTestButtonLabel,
onDebugEventSelected: this.onDebugEventSelected.bind(this)
}
})
.afterClosed()
.subscribe();
}
private updateNodeErrorTooltip(node: FcRuleNode) {
if (node.error) {
const element = $('#' + node.id);

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

@ -1403,7 +1403,7 @@
"target-entity": "Target entity",
"target-entities": "Target entities",
"alarm-type": "Alarm type",
"alarm-type-hint": "Unique identifier (e.g., HighTemperatureAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.",
"alarm-type-hint": "Unique identifier (e.g., HighTempAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.",
"alarm-type-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.",
"alarm-type-max-length": "Alarm type should be less than 256 characters.",
@ -1513,8 +1513,8 @@
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
"create-conditions": "Alarm Trigger Conditions",
"clear-condition": "Alarm Clear Condition",
"create-conditions": "Trigger conditions",
"clear-condition": "Clear condition",
"no-clear-alarm-rule": "No clear condition configured.",
"advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm to related entities",
@ -1532,6 +1532,8 @@
"alarm-rule-entity-list": "Entity list",
"missing-for": "missing for",
"time-unit": "Unit",
"mode": "Mode",
"type": "Type",
"value-required": "Value is required.",
"min-value": "Value must be 0 or greater.",
"argument-in-use": "Argument is used as general argument.",

Loading…
Cancel
Save