Browse Source

merge with feature/calculated-fields

pull/12678/head
IrynaMatveieva 2 years ago
parent
commit
65bd9d9886
  1. 3
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java
  3. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldTbelScriptEngine.java
  4. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java
  5. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  6. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  7. 79
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  8. 44
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java
  9. 30
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  10. 18
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java
  11. 12
      common/proto/src/main/proto/queue.proto
  12. 4
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  13. 9
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  14. 20
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java
  15. 22
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java
  16. 31
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java
  17. 32
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsDoubleVal.java
  18. 73
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java
  19. 7
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  20. 62
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  21. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  22. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html
  23. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss
  24. 14
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts
  25. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  26. 29
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  27. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts
  28. 36
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html
  29. 88
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts
  30. 99
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html
  31. 73
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss
  32. 157
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts
  33. 2
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html
  34. 14
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  35. 10
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  36. 8
      ui-ngx/src/app/shared/components/value-input.component.html
  37. 4
      ui-ngx/src/app/shared/components/value-input.component.ts
  38. 20
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  39. 9
      ui-ngx/src/app/shared/models/entity.models.ts
  40. 7
      ui-ngx/src/app/shared/models/rule-node.models.ts
  41. 1
      ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md
  42. 1
      ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md
  43. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

3
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -829,8 +829,7 @@ public class ActorSystemContext {
if (arguments != null) { if (arguments != null) {
eventBuilder.arguments(JacksonUtil.toString( eventBuilder.arguments(JacksonUtil.toString(
arguments.entrySet().stream() arguments.entrySet().stream()
.filter(entry -> !entry.getValue().isEmpty()) .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toTbelCfArg()))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getValue()))
)); ));
} }
if (result != null) { if (result != null) {

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -51,4 +52,6 @@ public interface ArgumentEntry {
return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); return new TsRollingArgumentEntry(kvEntries, limit, timeWindow);
} }
TbelCfArg toTbelCfArg();
} }

1
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldTbelScriptEngine.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import io.jsonwebtoken.lang.Collections;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.ScriptType;

1
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java

@ -59,7 +59,6 @@ public class RocksDBCalculatedFieldStateService implements CalculatedFieldStateS
if (state.isStateTooLarge()) { if (state.isStateTooLarge()) {
throw new CalculatedFieldStateException("State size exceeds the maximum allowed limit. The state will not be persisted to RocksDB."); throw new CalculatedFieldStateException("State size exceeds the maximum allowed limit. The state will not be persisted to RocksDB.");
} }
rocksDBService.put(toProto(stateId), toProto(stateId, state)); rocksDBService.put(toProto(stateId), toProto(stateId, state));
callback.onSuccess(); callback.onSuccess();
} }

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

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
@ -49,8 +50,9 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
Object[] args = ctx.getArgNames().stream() Object[] args = ctx.getArgNames().stream()
.map(key -> arguments.get(key).getValue()) .map(this::toTbelArgument)
.toArray(); .toArray();
ListenableFuture<Map<String, Object>> resultFuture = ctx.getCalculatedFieldScriptEngine().executeToMapAsync(args); ListenableFuture<Map<String, Object>> resultFuture = ctx.getCalculatedFieldScriptEngine().executeToMapAsync(args);
Output output = ctx.getOutput(); Output output = ctx.getOutput();
return Futures.transform(resultFuture, return Futures.transform(resultFuture,
@ -59,4 +61,8 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
); );
} }
private TbelCfArg toTbelArgument(String key) {
return arguments.get(key).toTbelCfArg();
}
} }

7
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
@ -74,6 +76,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
return isEmpty() ? null : kvEntryValue.getValue(); return isEmpty() ? null : kvEntryValue.getValue();
} }
@Override
public TbelCfArg toTbelCfArg() {
return new TbelCfSingleValueArg(ts, kvEntryValue.getValue());
}
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof SingleValueArgumentEntry singleValueEntry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) {

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

@ -20,16 +20,16 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.util.ProtoUtils;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@ -39,7 +39,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
private Integer limit; private Integer limit;
private Long timeWindow; private Long timeWindow;
private TreeMap<Long, BasicKvEntry> tsRecords = new TreeMap<>(); private TreeMap<Long, Double> tsRecords = new TreeMap<>();
public TsRollingArgumentEntry(List<TsKvEntry> kvEntries, int limit, long timeWindow) { public TsRollingArgumentEntry(List<TsKvEntry> kvEntries, int limit, long timeWindow) {
kvEntries.forEach(tsKvEntry -> addTsRecord(tsKvEntry.getTs(), tsKvEntry)); kvEntries.forEach(tsKvEntry -> addTsRecord(tsKvEntry.getTs(), tsKvEntry));
@ -47,7 +47,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
this.timeWindow = timeWindow; this.timeWindow = timeWindow;
} }
public TsRollingArgumentEntry(TreeMap<Long, BasicKvEntry> tsRecords, int limit, long timeWindow) { public TsRollingArgumentEntry(TreeMap<Long, Double> tsRecords, int limit, long timeWindow) {
this.tsRecords = tsRecords; this.tsRecords = tsRecords;
this.limit = limit; this.limit = limit;
this.timeWindow = timeWindow; this.timeWindow = timeWindow;
@ -72,57 +72,62 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
@JsonIgnore @JsonIgnore
@Override @Override
public Object getValue() { public Object getValue() {
return tsRecords.entrySet() return tsRecords;
.stream() }
.collect(Collectors.toMap(
Map.Entry::getKey, @Override
entry -> entry.getValue().getValue(), public TbelCfArg toTbelCfArg() {
(oldValue, newValue) -> oldValue, List<TbelCfTsDoubleVal> values = new ArrayList<>(tsRecords.size());
TreeMap::new for (var e : tsRecords.entrySet()) {
)); values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
}
return new TbelCfTsRollingArg(values);
} }
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof TsRollingArgumentEntry tsRollingEntry) { if (entry instanceof TsRollingArgumentEntry tsRollingEntry) {
return updateTsRollingEntry(tsRollingEntry); updateTsRollingEntry(tsRollingEntry);
} else if (entry instanceof SingleValueArgumentEntry singleValueEntry) { } else if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
return updateSingleValueEntry(singleValueEntry); updateSingleValueEntry(singleValueEntry);
} else { } else {
throw new IllegalArgumentException("Unsupported argument entry type for rolling argument entry: " + entry.getType()); throw new IllegalArgumentException("Unsupported argument entry type for rolling argument entry: " + entry.getType());
} }
return true;
} }
private boolean updateTsRollingEntry(TsRollingArgumentEntry tsRollingEntry) { private void updateTsRollingEntry(TsRollingArgumentEntry tsRollingEntry) {
boolean updated = false; for (Map.Entry<Long, Double> tsRecordEntry : tsRollingEntry.getTsRecords().entrySet()) {
for (Map.Entry<Long, BasicKvEntry> tsRecordEntry : tsRollingEntry.getTsRecords().entrySet()) { addTsRecord(tsRecordEntry.getKey(), tsRecordEntry.getValue());
updated |= addTsRecordIfAbsent(tsRecordEntry.getKey(), tsRecordEntry.getValue());
} }
return updated;
} }
private boolean updateSingleValueEntry(SingleValueArgumentEntry singleValueEntry) { private void updateSingleValueEntry(SingleValueArgumentEntry singleValueEntry) {
return addTsRecordIfAbsent(singleValueEntry.getTs(), singleValueEntry.getKvEntryValue()); addTsRecord(singleValueEntry.getTs(), singleValueEntry.getKvEntryValue());
} }
private boolean addTsRecordIfAbsent(Long ts, KvEntry value) { private void addTsRecord(Long ts, KvEntry value) {
if (!tsRecords.containsKey(ts)) { switch (value.getDataType()) {
addTsRecord(ts, value); case LONG -> value.getLongValue().ifPresent(aLong -> tsRecords.put(ts, aLong.doubleValue()));
return true; case DOUBLE -> value.getDoubleValue().ifPresent(aDouble -> tsRecords.put(ts, aDouble));
case BOOLEAN -> value.getBooleanValue().ifPresent(aBoolean -> tsRecords.put(ts, aBoolean ? 1.0 : 0.0));
case STRING -> value.getStrValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString)));
case JSON -> value.getJsonValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString)));
//TODO: try catch
} }
return false; cleanupExpiredRecords();
} }
private void addTsRecord(Long ts, KvEntry value) { private void addTsRecord(Long ts, double value) {
if (NumberUtils.isParsable(value.getValue().toString())) { tsRecords.put(ts, value);
tsRecords.put(ts, ProtoUtils.basicKvEntryFromKvEntry(value)); cleanupExpiredRecords();
if (tsRecords.size() > limit) { }
tsRecords.pollFirstEntry();
} private void cleanupExpiredRecords() {
tsRecords.entrySet().removeIf(tsRecord -> tsRecord.getKey() < System.currentTimeMillis() - timeWindow); if (tsRecords.size() > limit) {
} else { tsRecords.pollFirstEntry();
throw new IllegalArgumentException("Argument type " + getType() + " only supports numeric values.");
} }
tsRecords.entrySet().removeIf(tsRecord -> tsRecord.getKey() < System.currentTimeMillis() - timeWindow);
} }
} }

44
application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java

@ -26,8 +26,8 @@ import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentEntryProto; import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsValueListProto; import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto; import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
@ -90,17 +90,15 @@ public class CalculatedFieldUtils {
return builder.build(); return builder.build();
} }
public static TsRollingArgumentEntryProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) { public static TsRollingArgumentProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) {
TsValueListProto.Builder tsRecordsBuilder = TsValueListProto.newBuilder() TsRollingArgumentProto.Builder builder = TsRollingArgumentProto.newBuilder()
.setKey(argName); .setKey(argName)
.setLimit(entry.getLimit())
.setTimeWindow(entry.getTimeWindow());
entry.getTsRecords().forEach((ts, value) -> tsRecordsBuilder.addTsValue(KvProtoUtil.toTsValueProto(ts, value))); entry.getTsRecords().forEach((ts, value) -> builder.addTsValue(TsDoubleValProto.newBuilder().setTs(ts).setValue(value).build()));
return TsRollingArgumentEntryProto.newBuilder() return builder.build();
.setLimit(entry.getLimit())
.setTimeWindow(entry.getTimeWindow())
.setRecords(tsRecordsBuilder.build())
.build();
} }
public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) {
@ -120,27 +118,27 @@ public class CalculatedFieldUtils {
if (CalculatedFieldType.SCRIPT.equals(type)) { if (CalculatedFieldType.SCRIPT.equals(type)) {
proto.getRollingValueArgumentsList().forEach(argProto -> proto.getRollingValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getRecords().getKey(), fromRollingArgumentProto(argProto))); state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto)));
} }
return state; return state;
} }
public static SingleValueArgumentEntry fromSingleValueArgumentProto(SingleValueArgumentProto proto) { public static SingleValueArgumentEntry fromSingleValueArgumentProto(SingleValueArgumentProto proto) {
if (!proto.hasValue()) {
return new SingleValueArgumentEntry();
}
TsValueProto tsValueProto = proto.getValue(); TsValueProto tsValueProto = proto.getValue();
long ts = tsValueProto.getTs(); return new SingleValueArgumentEntry(
BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto); tsValueProto.getTs(),
return new SingleValueArgumentEntry(ts, kvEntry, proto.getVersion()); (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto),
proto.getVersion()
);
} }
public static TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentEntryProto proto) { public static TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentProto proto) {
TsValueListProto records = proto.getRecords(); TreeMap<Long, Double> tsRecords = new TreeMap<>();
String key = records.getKey(); proto.getTsValueList().forEach(tsValueProto -> tsRecords.put(tsValueProto.getTs(), tsValueProto.getValue()));
TreeMap<Long, BasicKvEntry> tsRecords = new TreeMap<>();
records.getTsValueList().forEach(tsValueProto -> {
BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(key, tsValueProto);
tsRecords.put(tsValueProto.getTs(), kvEntry);
});
return new TsRollingArgumentEntry(tsRecords, proto.getLimit(), proto.getTimeWindow()); return new TsRollingArgumentEntry(tsRecords, proto.getLimit(), proto.getTimeWindow());
} }

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

@ -134,10 +134,10 @@ public class ScriptCalculatedFieldStateTest {
void testPerformCalculationWhenOldTelemetry() throws ExecutionException, InterruptedException { void testPerformCalculationWhenOldTelemetry() throws ExecutionException, InterruptedException {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry(); TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, BasicKvEntry> values = new TreeMap<>(); TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 40000, new LongDataEntry("deviceTemperature", 4L));// will not be used for calculation values.put(ts - 40000, 4.0);// will not be used for calculation
values.put(ts - 45000, new LongDataEntry("deviceTemperature", 2L));// will not be used for calculation values.put(ts - 45000, 2.0);// will not be used for calculation
values.put(ts - 20, new LongDataEntry("deviceTemperature", 0L)); values.put(ts - 20, 0.0);
argumentEntry.setTsRecords(values); argumentEntry.setTsRecords(values);
@ -155,13 +155,13 @@ public class ScriptCalculatedFieldStateTest {
@Test @Test
void testPerformCalculationWhenArgumentsMoreThanLimit() throws ExecutionException, InterruptedException { void testPerformCalculationWhenArgumentsMoreThanLimit() throws ExecutionException, InterruptedException {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry(); TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, BasicKvEntry> values = new TreeMap<>(); TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 20, new LongDataEntry("deviceTemperature", 1000L));// will not be used values.put(ts - 20, 1000.0);// will not be used
values.put(ts - 18, new LongDataEntry("deviceTemperature", 0L)); values.put(ts - 18, 0.0);
values.put(ts - 16, new LongDataEntry("deviceTemperature", 0L)); values.put(ts - 16, 0.0);
values.put(ts - 14, new LongDataEntry("deviceTemperature", 0L)); values.put(ts - 14, 0.0);
values.put(ts - 12, new LongDataEntry("deviceTemperature", 0L)); values.put(ts - 12, 0.0);
values.put(ts - 10, new LongDataEntry("deviceTemperature", 0L)); values.put(ts - 10, 0.0);
argumentEntry.setTsRecords(values); argumentEntry.setTsRecords(values);
state.arguments = new HashMap<>(Map.of("deviceTemperature", argumentEntry, "assetHumidity", assetHumidityArgEntry)); state.arguments = new HashMap<>(Map.of("deviceTemperature", argumentEntry, "assetHumidity", assetHumidityArgEntry));
@ -198,10 +198,10 @@ public class ScriptCalculatedFieldStateTest {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry(); TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
TreeMap<Long, BasicKvEntry> values = new TreeMap<>(); TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 40, new LongDataEntry("deviceTemperature", 10L)); values.put(ts - 40, 10.0);
values.put(ts - 30, new LongDataEntry("deviceTemperature", 12L)); values.put(ts - 30, 12.0);
values.put(ts - 20, new LongDataEntry("deviceTemperature", 17L)); values.put(ts - 20, 17.0);
argumentEntry.setTsRecords(values); argumentEntry.setTsRecords(values);
return argumentEntry; return argumentEntry;

18
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java

@ -35,10 +35,10 @@ public class TsRollingArgumentEntryTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
TreeMap<Long, BasicKvEntry> values = new TreeMap<>(); TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 40, new DoubleDataEntry("key", 10.0)); values.put(ts - 40, 10.0);
values.put(ts - 30, new DoubleDataEntry("key", 12.0)); values.put(ts - 30, 12.0);
values.put(ts - 20, new DoubleDataEntry("key", 17.0)); values.put(ts - 20, 17.0);
// entry = new TsRollingArgumentEntry(values); // entry = new TsRollingArgumentEntry(values);
} }
@ -54,7 +54,7 @@ public class TsRollingArgumentEntryTest {
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.getTsRecords()).hasSize(4); assertThat(entry.getTsRecords()).hasSize(4);
assertThat(entry.getTsRecords().get(ts - 10).getValue()).isEqualTo(23.0); assertThat(entry.getTsRecords().get(ts - 10)).isEqualTo(23.0);
} }
@Test @Test
@ -67,10 +67,10 @@ public class TsRollingArgumentEntryTest {
@Test @Test
void testUpdateEntryWhenRollingEntryPassed() { void testUpdateEntryWhenRollingEntryPassed() {
TsRollingArgumentEntry newEntry = new TsRollingArgumentEntry(); TsRollingArgumentEntry newEntry = new TsRollingArgumentEntry();
TreeMap<Long, BasicKvEntry> values = new TreeMap<>(); TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 20, new DoubleDataEntry("key", 16.0)); values.put(ts - 20, 16.0);
values.put(ts - 10, new DoubleDataEntry("key", 7.0)); values.put(ts - 10, 7.0);
values.put(ts - 5, new DoubleDataEntry("key", 1.0)); values.put(ts - 5, 1.0);
newEntry.setTsRecords(values); newEntry.setTsRecords(values);
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry)).isTrue();

12
common/proto/src/main/proto/queue.proto

@ -814,10 +814,16 @@ message SingleValueArgumentProto {
int64 version = 3; int64 version = 3;
} }
message TsRollingArgumentEntryProto { message TsDoubleValProto {
TsValueListProto records = 1; int64 ts = 1;
double value = 2;
}
message TsRollingArgumentProto {
string key = 1;
int32 limit = 2; int32 limit = 2;
int64 timeWindow = 3; int64 timeWindow = 3;
repeated TsDoubleValProto tsValue = 4;
} }
message CalculatedFieldStateProto { message CalculatedFieldStateProto {
@ -825,7 +831,7 @@ message CalculatedFieldStateProto {
// int32 version = 2; // int32 version = 2;
string type = 3; string type = 3;
repeated SingleValueArgumentProto singleValueArguments = 4; repeated SingleValueArgumentProto singleValueArguments = 4;
repeated TsRollingArgumentEntryProto rollingValueArguments = 5; repeated TsRollingArgumentProto rollingValueArguments = 5;
} }
//Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level.

4
common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java

@ -22,6 +22,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfObject;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -256,6 +258,8 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
for (Object arg : args) { for (Object arg : args) {
if (arg instanceof CharSequence) { if (arg instanceof CharSequence) {
totalArgsSize += ((CharSequence) arg).length(); totalArgsSize += ((CharSequence) arg).length();
} else if (arg instanceof TbelCfObject tbelCfObj) {
totalArgsSize += tbelCfObj.memorySize();
} else { } else {
var str = JacksonUtil.toString(arg); var str = JacksonUtil.toString(arg);
if (str != null) { if (str != null) {

9
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java

@ -130,9 +130,12 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE); OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE);
parserConfig = ParserContext.enableSandboxedMode(); parserConfig = ParserContext.enableSandboxedMode();
parserConfig.addImport("JSON", TbJson.class); parserConfig.addImport("JSON", TbJson.class);
parserConfig.registerDataType("Date", TbDate.class, date -> 8L); parserConfig.registerDataType("Date", TbDate.class, val -> 8L);
parserConfig.registerDataType("Random", Random.class, date -> 8L); parserConfig.registerDataType("Random", Random.class, val -> 8L);
parserConfig.registerDataType("Calendar", Calendar.class, date -> 8L); parserConfig.registerDataType("Calendar", Calendar.class, val -> 8L);
parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize);
parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize);
TbUtils.register(parserConfig); TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
try { try {

20
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2024 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.script.api.tbel;
public interface TbelCfArg extends TbelCfObject {
}

22
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2024 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.script.api.tbel;
public interface TbelCfObject {
long memorySize();
}

31
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2024 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.script.api.tbel;
import lombok.Data;
@Data
public class TbelCfSingleValueArg implements TbelCfArg {
private final long ts;
private final Object value;
@Override
public long memorySize() {
return 8L; // TODO;
}
}

32
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsDoubleVal.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2024 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.script.api.tbel;
import lombok.Data;
@Data
public class TbelCfTsDoubleVal implements TbelCfObject {
public static final long OBJ_SIZE = 32L; // Approximate calculation;
private final long ts;
private final double value;
@Override
public long memorySize() {
return OBJ_SIZE;
}
}

73
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2024 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.script.api.tbel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal> {
@Getter
private final List<TbelCfTsDoubleVal> values;
public TbelCfTsRollingArg(List<TbelCfTsDoubleVal> values) {
this.values = Collections.unmodifiableList(values);
}
@Override
public long memorySize() {
return 12 + values.size() * OBJ_SIZE;
}
@JsonIgnore
public List<TbelCfTsDoubleVal> getValue() {
return values;
}
public double max() {
double max = Double.MIN_VALUE;
for (TbelCfTsDoubleVal value : values) {
double val = value.getValue();
if (max < val) {
max = val;
}
}
return max;
}
@JsonIgnore
public int getSize() {
return values.size();
}
@Override
public Iterator<TbelCfTsDoubleVal> iterator() {
return values.iterator();
}
@Override
public void forEach(Consumer<? super TbelCfTsDoubleVal> action) {
values.forEach(action);
}
}

7
ui-ngx/src/app/core/http/calculated-fields.service.ts

@ -19,9 +19,10 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { CalculatedField } from '@shared/models/calculated-field.models'; import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -48,4 +49,8 @@ export class CalculatedFieldsService {
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
return this.http.post<EntityTestScriptResult>('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config));
}
} }

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

@ -34,15 +34,20 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators'; import { catchError, filter, switchMap, tap } from 'rxjs/operators';
import { import {
CalculatedField, CalculatedField,
CalculatedFieldDebugDialogData, CalculatedFieldDebugDialogData,
CalculatedFieldDialogData CalculatedFieldDialogData,
CalculatedFieldTestScriptDialogData,
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { CalculatedFieldDebugDialogComponent, CalculatedFieldDialogComponent } from './components/public-api'; import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent
} from './components/public-api';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { CalculatedFieldId } from '@shared/models/id/calculated-field-id'; import { isObject } from '@core/utils';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
@ -53,7 +58,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
readonly tenantId = getCurrentAuthUser(this.store).tenantId; readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = { additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'), title: this.translate.instant('calculated-fields.see-debug-events'),
action: (id?: CalculatedFieldId) => this.openDebugDialog.call(this, id), action: (calculatedField: CalculatedField) => this.openDebugDialog.call(this, calculatedField),
}; };
constructor(private calculatedFieldsService: CalculatedFieldsService, constructor(private calculatedFieldsService: CalculatedFieldsService,
@ -134,10 +139,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink);
} }
onOpenDebugConfig($event: Event, { debugSettings = {}, id }: CalculatedField): void { onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = { const additionalActionConfig = {
...this.additionalDebugActionConfig, ...this.additionalDebugActionConfig,
action: () => this.openDebugDialog(id) action: () => this.openDebugDialog(calculatedField)
}; };
const { viewContainerRef } = this.getTable(); const { viewContainerRef } = this.getTable();
if ($event) { if ($event) {
@ -173,8 +179,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
) )
} }
private editCalculatedField(calculatedField: CalculatedField): void { private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
this.getCalculatedFieldDialog(calculatedField, 'action.apply') this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
.pipe( .pipe(
filter(Boolean), filter(Boolean),
switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField })), switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField })),
@ -186,7 +192,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}); });
} }
private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add'): Observable<CalculatedField> { private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> {
return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, { return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
@ -198,19 +204,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName, entityName: this.entityName,
additionalDebugActionConfig: this.additionalDebugActionConfig, additionalDebugActionConfig: this.additionalDebugActionConfig,
} getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty
},
enterAnimationDuration: isDirty ? 0 : null,
}) })
.afterClosed(); .afterClosed();
} }
private openDebugDialog(id: CalculatedFieldId): void { private openDebugDialog(calculatedField: CalculatedField): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, { this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
tenantId: this.tenantId, tenantId: this.tenantId,
entityId: this.entityId, value: calculatedField,
id getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
} }
}) })
.afterClosed() .afterClosed()
@ -251,4 +260,29 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
takeUntilDestroyed(this.destroyRef), takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData()); ).subscribe(() => this.updateData());
} }
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>, openCalculatedFieldEdit = true): Observable<string> {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) ? argumentsObj[key] : '';
return acc;
}, {});
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression: calculatedField.configuration.expression,
openCalculatedFieldEdit
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField({...calculatedField, configuration: {...calculatedField.configuration, expression } }, true)
}
}),
);
}
} }

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

@ -24,7 +24,7 @@
<div class="tb-form-table-header-cell w-1/6">{{ 'entity.key' | translate }}</div> <div class="tb-form-table-header-cell w-1/6">{{ 'entity.key' | translate }}</div>
<div class="tb-form-table-header-cell w-24 min-w-24"></div> <div class="tb-form-table-header-cell w-24 min-w-24"></div>
</div> </div>
<div class="tb-form-table-body tb-drop-list"> <div class="tb-form-table-body">
@for (group of argumentsFormArray.controls; track group) { @for (group of argumentsFormArray.controls; track group) {
<div [formGroup]="group" class="tb-form-table-row"> <div [formGroup]="group" class="tb-form-table-row">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="debug-dialog-container w-screen max-w-5xl"> <div class="w-screen max-w-5xl">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ 'calculated-fields.debugging' | translate}}</h2> <h2>{{ 'calculated-fields.debugging' | translate}}</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
@ -25,15 +25,16 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content class="tb-form-panel stroked debug-dialog-content h-full"> <div mat-dialog-content class="tb-form-panel stroked debug-dialog-content">
<tb-event-table <tb-event-table
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]" [debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]" [disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD" [defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[active]="true" [active]="true"
[entityId]="data.id" [entityId]="data.value.id"
[functionTestButtonLabel]="'common.test-function' | translate" [functionTestButtonLabel]="'common.test-function' | translate"
(debugEventSelected)="onDebugEventSelected($event)"
/> />
</div> </div>
<div mat-dialog-actions class="justify-end"> <div mat-dialog-actions class="justify-end">

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

@ -14,12 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.debug-dialog-container { .debug-dialog-content {
height: 77vh; height: 65vh;
min-width: 80vw; border-radius: 0;
.debug-dialog-content {
border-radius: 0;
}
} }
} }

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

@ -20,16 +20,16 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import { EventTableComponent } from '@home/components/event/event-table.component'; import { EventTableComponent } from '@home/components/event/event-table.component';
import { CalculatedFieldDebugDialogData } from '@shared/models/calculated-field.models'; import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models';
@Component({ @Component({
selector: 'tb-calculated-field-debug-dialog', selector: 'tb-calculated-field-debug-dialog',
styleUrls: ['calculated-field-debug-dialog.component.scss'], styleUrls: ['calculated-field-debug-dialog.component.scss'],
templateUrl: './calculated-field-debug-dialog.component.html', templateUrl: './calculated-field-debug-dialog.component.html',
}) })
export class CalculatedFieldDebugDialogComponent extends DialogComponent<CalculatedFieldDebugDialogComponent, null> implements AfterViewInit { export class CalculatedFieldDebugDialogComponent extends DialogComponent<CalculatedFieldDebugDialogComponent, string> implements AfterViewInit {
@ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent;
@ -40,15 +40,21 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent<Calcula
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, null>) { protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, string>) {
super(store, router, dialogRef); super(store, router, dialogRef);
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.eventsTable.entitiesTable.updateData(); this.eventsTable.entitiesTable.updateData();
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = () => this.data.value.type === CalculatedFieldType.SCRIPT;
} }
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
onDebugEventSelected(event: CalculatedFieldEventBody): void {
this.data.getTestScriptDialogFn(this.data.value, JSON.parse(event.arguments))
.subscribe(expression => this.dialogRef.close(expression));
}
} }

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

@ -99,8 +99,16 @@
[functionArgs]="functionArgs$ | async" [functionArgs]="functionArgs$ | async"
[disableUndefinedCheck]="true" [disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL" [scriptLanguage]="ScriptLanguage.TBEL"
helpId="[TODO]: [Calculated Fields] add valid link" helpId="calculated-field/expression_fn"
/> />
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="configFormGroup.get('expressionSCRIPT').invalid || configFormGroup.get('arguments').invalid ">
{{ 'common.test-function' | translate }}
</button>
</div>
} }
</div> </div>
<div class="tb-form-panel" [formGroup]="outputFormGroup"> <div class="tb-form-panel" [formGroup]="outputFormGroup">

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { AfterViewInit, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -41,7 +41,7 @@ import { ScriptLanguage } from '@shared/models/rule-node.models';
selector: 'tb-calculated-field-dialog', selector: 'tb-calculated-field-dialog',
templateUrl: './calculated-field-dialog.component.html', templateUrl: './calculated-field-dialog.component.html',
}) })
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> { export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> implements AfterViewInit {
fieldFormGroup = this.fb.group({ fieldFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
@ -67,7 +67,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
additionalDebugActionConfig = this.data.value?.id ? { additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig, ...this.data.additionalDebugActionConfig,
action: () => this.data.additionalDebugActionConfig.action(this.data.value.id) action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
} : null; } : null;
readonly OutputTypeTranslations = OutputTypeTranslations; readonly OutputTypeTranslations = OutputTypeTranslations;
@ -98,18 +98,35 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
return this.fieldFormGroup.get('configuration').get('output') as FormGroup; return this.fieldFormGroup.get('configuration').get('output') as FormGroup;
} }
get fromGroupValue(): CalculatedField {
const { configuration, type, ...rest } = this.fieldFormGroup.value;
const { expressionSIMPLE, expressionSCRIPT, ...restConfig } = configuration;
return { configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type } as CalculatedField;
}
ngAfterViewInit(): void {
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
add(): void { add(): void {
if (this.fieldFormGroup.valid) { if (this.fieldFormGroup.valid) {
const { configuration, type, ...rest } = this.fieldFormGroup.value; this.dialogRef.close(this.fromGroupValue);
const { expressionSIMPLE, expressionSCRIPT, ...restConfig } = configuration;
this.dialogRef.close({ configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type } as CalculatedField);
} }
} }
onTestScript(): void {
this.data.getTestScriptDialogFn(this.fromGroupValue, null, false).subscribe(expression => {
this.configFormGroup.get('expressionSCRIPT').setValue(expression);
this.configFormGroup.get('expressionSCRIPT').markAsDirty();
});
}
private applyDialogData(): void { private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value ?? {}; const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value ?? {};
const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration; const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration;

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

@ -18,3 +18,4 @@ export * from './dialog/calculated-field-dialog.component';
export * from './arguments-table/calculated-field-arguments-table.component'; export * from './arguments-table/calculated-field-arguments-table.component';
export * from './panel/calculated-field-argument-panel.component'; export * from './panel/calculated-field-argument-panel.component';
export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './debug-dialog/calculated-field-debug-dialog.component';
export * from './test-dialog/calculated-field-script-test-dialog.component';

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

@ -0,0 +1,36 @@
<!--
Copyright © 2016-2024 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.
-->
<div class="flex flex-col gap-2 px-4">
<div>{{ 'calculated-fields.arguments' | translate }}</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell w-1/4">{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-form-table-header-cell flex-1">{{ 'common.value' | translate }}</div>
</div>
<div class="tb-form-table-body">
@for (group of argumentsFormArray.controls; track group) {
<div [formGroup]="group" class="tb-form-table-row">
<mat-form-field appearance="outline" class="tb-inline-field w-1/4" subscriptSizing="dynamic">
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
</mat-form-field>
<tb-value-input class="flex-1" [required]="false" formControlName="value"/>
</div>
}
</div>
</div>
</div>

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

@ -0,0 +1,88 @@
///
/// Copyright © 2016-2024 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 { Component, forwardRef } from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
ValidationErrors,
FormBuilder,
FormGroup
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-calculated-field-test-arguments',
templateUrl: './calculated-field-test-arguments.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent),
multi: true,
}
]
})
export class CalculatedFieldTestArgumentsComponent extends PageComponent implements ControlValueAccessor, Validator {
argumentsFormArray = this.fb.array<FormGroup>([]);
private propagateChange: (value: { argumentName: string; value: unknown }) => void;
constructor(private fb: FormBuilder) {
super();
this.argumentsFormArray.valueChanges
.pipe(takeUntilDestroyed())
.subscribe(() => this.propagateChange(this.getValue()));
}
registerOnChange(propagateChange: (value: { argumentName: string; value: unknown }) => void): void {
this.propagateChange = propagateChange;
}
registerOnTouched(_): void {
}
writeValue(argumentsObj: Record<string, unknown>): void {
this.argumentsFormArray.clear();
Object.keys(argumentsObj).forEach(key => {
this.argumentsFormArray.push(this.fb.group({
argumentName: [{ value: key, disabled: true}],
value: [argumentsObj[key]]
}) as FormGroup, {emitEvent: false});
});
}
validate(): ValidationErrors | null {
return this.argumentsFormArray.valid ? null : { arguments: { valid: false } };
}
private getValue(): { argumentName: string; value: unknown } {
return this.argumentsFormArray.getRawValue().reduce((acc, rowItem) => {
const { argumentName, value } = rowItem;
acc[argumentName] = value;
return acc;
}, {}) as { argumentName: string; value: unknown };
}
}

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

@ -0,0 +1,99 @@
<!--
Copyright © 2016-2024 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.
-->
<form class="test-dialog-container size-full" [formGroup]="calculatedFieldScriptTestFormGroup">
<mat-toolbar class="flex justify-between" color="primary">
<h2>{{ 'calculated-fields.test-script-function' | translate }} ({{ 'api-usage.tbel' | translate }})</h2>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content class="relative">
<div class="tb-absolute-fill">
<div class="tb-fullscreen-panel flex size-full flex-row">
<div #leftPanel class="test-block-content overflow-hidden">
<div class="relative size-full min-w-64">
<div class="block-label-container left">
<span class="block-label">{{ 'calculated-fields.expression' | translate }}</span>
</div>
<tb-js-func
#expressionContent
formControlName="expression"
functionName="calculate"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[fillHeight]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
resultType="object"
helpId="calculated-field/test-expression_fn"
/>
</div>
</div>
<div #rightPanel>
<div #topRightPanel class="test-block-content">
<div class="relative flex size-full min-w-96 gap-2">
<div class="block-label-container right-top">
<span class="block-label">{{ 'calculated-fields.arguments' | translate }}</span>
</div>
<tb-calculated-field-test-arguments class="size-full" formControlName="arguments"/>
</div>
</div>
<div #bottomRightPanel class="test-block-content">
<div class="relative size-full">
<div class="block-label-container right-bottom">
<span class="block-label" translate>common.output</span>
</div>
<tb-json-content
class="flex-1"
formControlName="output"
label="{{ 'common.output' | translate }}"
[contentType]="ContentType.JSON"
validateContent="false"
readonly="true"
[fillHeight]="true"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div mat-dialog-actions class="flex flex-row">
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="(isLoading$ | async) || calculatedFieldScriptTestFormGroup.invalid">
{{ 'action.test' | translate }}
</button>
<span class="flex-1"></span>
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
type="submit"
(click)="save()"
[disabled]="(isLoading$ | async) || calculatedFieldScriptTestFormGroup.get('expression').invalid || !calculatedFieldScriptTestFormGroup.get('expression').dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

73
ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.test-dialog-container {
.block-label {
padding: 4px;
color: #00acc1;
background: rgba(220, 220, 220, .35);
border-radius: 5px;
}
.test-block-content {
padding-top: 5px;
padding-left: 5px;
border: 1px solid #c0c0c0;
}
.block-label-container {
position: absolute;
z-index: 10;
font-size: 12px;
font-weight: bold;
&.left {
right: 112px;
top: 9px;
}
&.right-bottom {
right: 40px;
top: 6px;
}
&.right-top {
right: 8px;
top: 2px;
}
}
}
}
:host::ng-deep {
.test-dialog-container {
.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-horizontal {
cursor: col-resize;
background-image: url("../../../../../../../assets/split.js/grips/horizontal.png");
}
.gutter.gutter-vertical {
cursor: row-resize;
background-image: url("../../../../../../../assets/split.js/grips/vertical.png");
}
}
}

157
ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts

@ -0,0 +1,157 @@
///
/// Copyright © 2016-2024 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,
DestroyRef,
ElementRef,
Inject,
ViewChild,
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder } from '@angular/forms';
import { NEVER, Observable, of, switchMap } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { ContentType } from '@shared/models/constants';
import { JsonContentComponent } from '@shared/components/json-content.component';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { beautifyJs } from '@shared/models/beautify.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs/operators';
import { CalculatedFieldTestScriptDialogData } from '@shared/models/calculated-field.models';
@Component({
selector: 'tb-calculated-field-script-test-dialog',
templateUrl: './calculated-field-script-test-dialog.component.html',
styleUrls: ['./calculated-field-script-test-dialog.component.scss'],
})
export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<CalculatedFieldScriptTestDialogComponent,
string> implements AfterViewInit {
@ViewChild('leftPanel', {static: true}) leftPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('rightPanel', {static: true}) rightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('topRightPanel', {static: true}) topRightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('bottomRightPanel', {static: true}) bottomRightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('expressionContent', {static: true}) expressionContent: JsonContentComponent;
calculatedFieldScriptTestFormGroup = this.fb.group({
expression: [],
arguments: [],
output: []
});
readonly ContentType = ContentType;
readonly ScriptLanguage = ScriptLanguage;
readonly functionArgs = Object.keys(this.data.arguments);
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldTestScriptDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldScriptTestDialogComponent, string>,
private dialog: MatDialog,
private fb: FormBuilder,
private destroyRef: DestroyRef,
private calculatedFieldService: CalculatedFieldsService) {
super(store, router, dialogRef);
beautifyJs(this.data.expression, {indent_size: 4}).pipe(filter(Boolean), takeUntilDestroyed()).subscribe(
(res) => this.calculatedFieldScriptTestFormGroup.get('expression').patchValue(res, {emitEvent: false})
);
this.calculatedFieldScriptTestFormGroup.get('arguments').patchValue(this.data.arguments, {emitEvent: false});
}
ngAfterViewInit(): void {
this.initSplitLayout(
this.leftPanelElmRef.nativeElement,
this.rightPanelElmRef.nativeElement,
this.topRightPanelElmRef.nativeElement,
this.bottomRightPanelElmRef.nativeElement
);
}
cancel(): void {
this.dialogRef.close(null);
}
onTestScript(): void {
this.testScript()
.pipe(
switchMap(output => beautifyJs(output, {indent_size: 4})),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(output => this.calculatedFieldScriptTestFormGroup.get('output').setValue(output));
}
save(): void {
this.testScript(true).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.calculatedFieldScriptTestFormGroup.get('expression').markAsPristine();
this.dialogRef.close(this.calculatedFieldScriptTestFormGroup.get('expression').value);
});
}
private testScript(onSave = false): Observable<string> {
if (this.checkInputParamErrors()) {
return this.calculatedFieldService.testScript({
expression: this.calculatedFieldScriptTestFormGroup.get('expression').value,
arguments: this.calculatedFieldScriptTestFormGroup.get('arguments').value
}).pipe(
switchMap(result => {
if (result.error) {
this.store.dispatch(new ActionNotificationShow(
{
message: result.error,
type: 'error'
}));
return NEVER;
} else {
if (onSave && this.data.openCalculatedFieldEdit) {
this.dialog.closeAll();
}
return of(result.output);
}
}),
);
} else {
return NEVER;
}
}
private checkInputParamErrors(): boolean {
this.expressionContent.validateOnSubmit();
return !this.calculatedFieldScriptTestFormGroup.get('expression').invalid;
}
private initSplitLayout(leftPanel, rightPanel, topRightPanel, bottomRightPanel): void {
Split([leftPanel, rightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize'
});
Split([topRightPanel, bottomRightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'row-resize',
direction: 'vertical'
});
}
}

2
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html

@ -54,7 +54,7 @@
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"
(click)="additionalActionConfig.action()"> (click)="onCancel(); additionalActionConfig.action()">
{{ additionalActionConfig.title | translate }} {{ additionalActionConfig.title | translate }}
</button> </button>
} }

14
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -357,10 +357,10 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
break; break;
case DebugEventType.DEBUG_CALCULATED_FIELD: case DebugEventType.DEBUG_CALCULATED_FIELD:
this.columns[0].width = '80px'; this.columns[0].width = '80px';
this.columns[1].width = '20%'; this.columns[1].width = '100px';
this.columns.push( this.columns.push(
new EntityTableColumn<Event>('entityId', 'event.entity-id', '85px', new EntityTableColumn<Event>('entityId', 'event.entity-id', '85px',
(entity) => `<span style="display: inline-block; width: 7ch">${entity.body.entityId.substring(0, 6)}…</span>`, (entity) => `<span style="display: inline-block; width: 9ch">${entity.body.entityId.substring(0, 8)}…</span>`,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
false, false,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
@ -380,7 +380,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
} }
), ),
new EntityTableColumn<Event>('messageId', 'event.message-id', '85px', new EntityTableColumn<Event>('messageId', 'event.message-id', '85px',
(entity) => `<span style="display: inline-block; width: 7ch">${entity.body.msgId?.substring(0, 6)}…</span>`, (entity) => `<span style="display: inline-block; width: 9ch">${entity.body.msgId?.substring(0, 8)}…</span>`,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
false, false,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
@ -457,6 +457,14 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
}); });
} }
break; break;
case DebugEventType.DEBUG_CALCULATED_FIELD:
this.cellActionDescriptors.push({
name: this.translate.instant('common.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}),
icon: 'bug_report',
isEnabled: () => true,
onAction: (_, entity) => this.debugEventSelected.next(entity.body)
});
break;
} }
this.getTable()?.cellActionDescriptorsUpdated(); this.getTable()?.cellActionDescriptorsUpdated();
} }

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

@ -198,6 +198,12 @@ import {
import { import {
CalculatedFieldDebugDialogComponent CalculatedFieldDebugDialogComponent
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component'; } from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component';
import {
CalculatedFieldScriptTestDialogComponent
} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component';
import {
CalculatedFieldTestArgumentsComponent
} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -347,6 +353,8 @@ import {
CalculatedFieldArgumentsTableComponent, CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent, CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -490,6 +498,8 @@ import {
CalculatedFieldArgumentsTableComponent, CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent, CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

8
ui-ngx/src/app/shared/components/value-input.component.html

@ -32,7 +32,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.STRING" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1"> <mat-form-field *ngIf="valueType === valueTypeEnum.STRING" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<input [disabled]="disabled" matInput required name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" <input [disabled]="disabled" matInput [required]="required" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.string-value' | translate }}*"/> placeholder="{{ 'value.string-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
@ -44,7 +44,7 @@
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.INTEGER" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1"> <mat-form-field *ngIf="valueType === valueTypeEnum.INTEGER" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1">
<input [disabled]="disabled" matInput required name="value" type="number" step="1" pattern="^-?[0-9]+$" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" <input [disabled]="disabled" matInput [required]="required" name="value" type="number" step="1" pattern="^-?[0-9]+$" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.integer-value' | translate }}*"/> placeholder="{{ 'value.integer-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
@ -57,7 +57,7 @@
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.DOUBLE" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1"> <mat-form-field *ngIf="valueType === valueTypeEnum.DOUBLE" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1">
<input [disabled]="disabled" matInput required name="value" type="number" step="any" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" <input [disabled]="disabled" matInput [required]="required" name="value" type="number" step="any" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.double-value' | translate }}*"/> placeholder="{{ 'value.double-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
@ -80,7 +80,7 @@
</ng-container> </ng-container>
<div *ngIf="valueType === valueTypeEnum.JSON" class="flex flex-1 flex-row items-center justify-start gap-2"> <div *ngIf="valueType === valueTypeEnum.JSON" class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1"> <mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<input [disabled]="disabled" matInput tb-json-to-string required name="value" #value="ngModel" <input [disabled]="disabled" matInput tb-json-to-string [required]="required" name="value" #value="ngModel"
[(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" placeholder="{{ 'value.json-value' | translate }}*"/> [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" placeholder="{{ 'value.json-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"

4
ui-ngx/src/app/shared/components/value-input.component.ts

@ -81,6 +81,10 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro
@coerceBoolean() @coerceBoolean()
shortBooleanField = false; shortBooleanField = false;
@Input()
@coerceBoolean()
required = true;
@Input() @Input()
layout: ValueInputLayout | Layout = 'row'; layout: ValueInputLayout | Layout = 'row';

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

@ -26,6 +26,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType } from '@shared/models/alias.models'; import { AliasFilterType } from '@shared/models/alias.models';
import { Observable } from 'rxjs';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> { export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
debugSettings?: EntityDebugSettings; debugSettings?: EntityDebugSettings;
@ -126,6 +127,8 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument {
argumentName: string; argumentName: string;
} }
export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>;
export interface CalculatedFieldDialogData { export interface CalculatedFieldDialogData {
value?: CalculatedField; value?: CalculatedField;
buttonTitle: string; buttonTitle: string;
@ -133,13 +136,24 @@ export interface CalculatedFieldDialogData {
debugLimitsConfiguration: string; debugLimitsConfiguration: string;
tenantId: string; tenantId: string;
entityName?: string; entityName?: string;
additionalDebugActionConfig: AdditionalDebugActionConfig; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
isDirty?: boolean;
} }
export interface CalculatedFieldDebugDialogData { export interface CalculatedFieldDebugDialogData {
id?: CalculatedFieldId;
entityId: EntityId;
tenantId: string; tenantId: string;
value: CalculatedField;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
}
export interface CalculatedFieldTestScriptInputParams {
arguments: Record<string, unknown>,
expression: string;
}
export interface CalculatedFieldTestScriptDialogData extends CalculatedFieldTestScriptInputParams {
openCalculatedFieldEdit?: boolean;
} }
export interface ArgumentEntityTypeParams { export interface ArgumentEntityTypeParams {

9
ui-ngx/src/app/shared/models/entity.models.ts

@ -203,8 +203,13 @@ export interface EntityDebugSettings {
allEnabledUntil?: number; allEnabledUntil?: number;
} }
export interface AdditionalDebugActionConfig { export interface EntityTestScriptResult {
action?: (id?: EntityId) => void; output: string;
error: string;
}
export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => void> {
action: Action;
title: string; title: string;
} }

7
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -27,7 +27,7 @@ import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models'; import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { HasEntityDebugSettings } from '@shared/models/entity.models'; import { EntityTestScriptResult, HasEntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface RuleNodeConfiguration { export interface RuleNodeConfiguration {
@ -374,10 +374,7 @@ export interface TestScriptInputParams {
msgType: string; msgType: string;
} }
export interface TestScriptResult { export type TestScriptResult = EntityTestScriptResult;
output: string;
error: string;
}
export enum MessageType { export enum MessageType {
POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST', POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST',

1
ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md

@ -0,0 +1 @@
<!-- [TODO]: [Calculated Fields] add content -->

1
ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md

@ -0,0 +1 @@
<!-- [TODO]: [Calculated Fields] add content -->

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

@ -65,6 +65,7 @@
"next-with-label": "Next: {{label}}", "next-with-label": "Next: {{label}}",
"read-more": "Read more", "read-more": "Read more",
"hide": "Hide", "hide": "Hide",
"test": "Test",
"done": "Done", "done": "Done",
"print": "Print", "print": "Print",
"restore": "Restore", "restore": "Restore",
@ -1018,6 +1019,7 @@
"argument-name": "Argument name", "argument-name": "Argument name",
"datasource": "Datasource", "datasource": "Datasource",
"add-argument": "Add argument", "add-argument": "Add argument",
"test-script-function": "Test script function",
"no-arguments": "No arguments configured", "no-arguments": "No arguments configured",
"argument-settings": "Argument settings", "argument-settings": "Argument settings",
"argument-current": "Current entity", "argument-current": "Current entity",
@ -1107,8 +1109,12 @@
"proceed": "Proceed", "proceed": "Proceed",
"open-details-page": "Open details page", "open-details-page": "Open details page",
"not-found": "Not found", "not-found": "Not found",
"value": "Value",
"documentation": "Documentation", "documentation": "Documentation",
"time-left": "{{time}} left", "time-left": "{{time}} left",
"output": "Output",
"test-function": "Test function",
"test-with-this-message": "{{test}} with this message",
"suffix": { "suffix": {
"s": "s", "s": "s",
"ms": "ms" "ms": "ms"

Loading…
Cancel
Save