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) {
eventBuilder.arguments(JacksonUtil.toString(
arguments.entrySet().stream()
.filter(entry -> !entry.getValue().isEmpty())
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toTbelCfArg()))
));
}
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.JsonSubTypes;
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.TsKvEntry;
@ -51,4 +52,6 @@ public interface ArgumentEntry {
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.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.jsonwebtoken.lang.Collections;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
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()) {
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));
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.NoArgsConstructor;
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.configuration.Output;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
@ -49,8 +50,9 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
Object[] args = ctx.getArgNames().stream()
.map(key -> arguments.get(key).getValue())
.map(this::toTbelArgument)
.toArray();
ListenableFuture<Map<String, Object>> resultFuture = ctx.getCalculatedFieldScriptEngine().executeToMapAsync(args);
Output output = ctx.getOutput();
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.Data;
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.BasicKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
@ -74,6 +76,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
return isEmpty() ? null : kvEntryValue.getValue();
}
@Override
public TbelCfArg toTbelCfArg() {
return new TbelCfSingleValueArg(ts, kvEntryValue.getValue());
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
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.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.script.api.tbel.TbelCfArg;
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.TsKvEntry;
import org.thingsboard.server.common.util.ProtoUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@ -39,7 +39,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
private Integer limit;
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) {
kvEntries.forEach(tsKvEntry -> addTsRecord(tsKvEntry.getTs(), tsKvEntry));
@ -47,7 +47,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
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.limit = limit;
this.timeWindow = timeWindow;
@ -72,57 +72,62 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
@JsonIgnore
@Override
public Object getValue() {
return tsRecords.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().getValue(),
(oldValue, newValue) -> oldValue,
TreeMap::new
));
return tsRecords;
}
@Override
public TbelCfArg toTbelCfArg() {
List<TbelCfTsDoubleVal> values = new ArrayList<>(tsRecords.size());
for (var e : tsRecords.entrySet()) {
values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
}
return new TbelCfTsRollingArg(values);
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof TsRollingArgumentEntry tsRollingEntry) {
return updateTsRollingEntry(tsRollingEntry);
updateTsRollingEntry(tsRollingEntry);
} else if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
return updateSingleValueEntry(singleValueEntry);
updateSingleValueEntry(singleValueEntry);
} else {
throw new IllegalArgumentException("Unsupported argument entry type for rolling argument entry: " + entry.getType());
}
return true;
}
private boolean updateTsRollingEntry(TsRollingArgumentEntry tsRollingEntry) {
boolean updated = false;
for (Map.Entry<Long, BasicKvEntry> tsRecordEntry : tsRollingEntry.getTsRecords().entrySet()) {
updated |= addTsRecordIfAbsent(tsRecordEntry.getKey(), tsRecordEntry.getValue());
private void updateTsRollingEntry(TsRollingArgumentEntry tsRollingEntry) {
for (Map.Entry<Long, Double> tsRecordEntry : tsRollingEntry.getTsRecords().entrySet()) {
addTsRecord(tsRecordEntry.getKey(), tsRecordEntry.getValue());
}
return updated;
}
private boolean updateSingleValueEntry(SingleValueArgumentEntry singleValueEntry) {
return addTsRecordIfAbsent(singleValueEntry.getTs(), singleValueEntry.getKvEntryValue());
private void updateSingleValueEntry(SingleValueArgumentEntry singleValueEntry) {
addTsRecord(singleValueEntry.getTs(), singleValueEntry.getKvEntryValue());
}
private boolean addTsRecordIfAbsent(Long ts, KvEntry value) {
if (!tsRecords.containsKey(ts)) {
addTsRecord(ts, value);
return true;
private void addTsRecord(Long ts, KvEntry value) {
switch (value.getDataType()) {
case LONG -> value.getLongValue().ifPresent(aLong -> tsRecords.put(ts, aLong.doubleValue()));
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) {
if (NumberUtils.isParsable(value.getValue().toString())) {
tsRecords.put(ts, ProtoUtils.basicKvEntryFromKvEntry(value));
if (tsRecords.size() > limit) {
tsRecords.pollFirstEntry();
}
tsRecords.entrySet().removeIf(tsRecord -> tsRecord.getKey() < System.currentTimeMillis() - timeWindow);
} else {
throw new IllegalArgumentException("Argument type " + getType() + " only supports numeric values.");
private void addTsRecord(Long ts, double value) {
tsRecords.put(ts, value);
cleanupExpiredRecords();
}
private void cleanupExpiredRecords() {
if (tsRecords.size() > limit) {
tsRecords.pollFirstEntry();
}
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.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentEntryProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsValueListProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
@ -90,17 +90,15 @@ public class CalculatedFieldUtils {
return builder.build();
}
public static TsRollingArgumentEntryProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) {
TsValueListProto.Builder tsRecordsBuilder = TsValueListProto.newBuilder()
.setKey(argName);
public static TsRollingArgumentProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) {
TsRollingArgumentProto.Builder builder = TsRollingArgumentProto.newBuilder()
.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()
.setLimit(entry.getLimit())
.setTimeWindow(entry.getTimeWindow())
.setRecords(tsRecordsBuilder.build())
.build();
return builder.build();
}
public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) {
@ -120,27 +118,27 @@ public class CalculatedFieldUtils {
if (CalculatedFieldType.SCRIPT.equals(type)) {
proto.getRollingValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getRecords().getKey(), fromRollingArgumentProto(argProto)));
state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto)));
}
return state;
}
public static SingleValueArgumentEntry fromSingleValueArgumentProto(SingleValueArgumentProto proto) {
if (!proto.hasValue()) {
return new SingleValueArgumentEntry();
}
TsValueProto tsValueProto = proto.getValue();
long ts = tsValueProto.getTs();
BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto);
return new SingleValueArgumentEntry(ts, kvEntry, proto.getVersion());
return new SingleValueArgumentEntry(
tsValueProto.getTs(),
(BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto),
proto.getVersion()
);
}
public static TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentEntryProto proto) {
TsValueListProto records = proto.getRecords();
String key = records.getKey();
TreeMap<Long, BasicKvEntry> tsRecords = new TreeMap<>();
records.getTsValueList().forEach(tsValueProto -> {
BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(key, tsValueProto);
tsRecords.put(tsValueProto.getTs(), kvEntry);
});
public static TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentProto proto) {
TreeMap<Long, Double> tsRecords = new TreeMap<>();
proto.getTsValueList().forEach(tsValueProto -> tsRecords.put(tsValueProto.getTs(), tsValueProto.getValue()));
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 {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, BasicKvEntry> values = new TreeMap<>();
values.put(ts - 40000, new LongDataEntry("deviceTemperature", 4L));// will not be used for calculation
values.put(ts - 45000, new LongDataEntry("deviceTemperature", 2L));// will not be used for calculation
values.put(ts - 20, new LongDataEntry("deviceTemperature", 0L));
TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 40000, 4.0);// will not be used for calculation
values.put(ts - 45000, 2.0);// will not be used for calculation
values.put(ts - 20, 0.0);
argumentEntry.setTsRecords(values);
@ -155,13 +155,13 @@ public class ScriptCalculatedFieldStateTest {
@Test
void testPerformCalculationWhenArgumentsMoreThanLimit() throws ExecutionException, InterruptedException {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, BasicKvEntry> values = new TreeMap<>();
values.put(ts - 20, new LongDataEntry("deviceTemperature", 1000L));// will not be used
values.put(ts - 18, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 16, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 14, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 12, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 10, new LongDataEntry("deviceTemperature", 0L));
TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 20, 1000.0);// will not be used
values.put(ts - 18, 0.0);
values.put(ts - 16, 0.0);
values.put(ts - 14, 0.0);
values.put(ts - 12, 0.0);
values.put(ts - 10, 0.0);
argumentEntry.setTsRecords(values);
state.arguments = new HashMap<>(Map.of("deviceTemperature", argumentEntry, "assetHumidity", assetHumidityArgEntry));
@ -198,10 +198,10 @@ public class ScriptCalculatedFieldStateTest {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
long ts = System.currentTimeMillis();
TreeMap<Long, BasicKvEntry> values = new TreeMap<>();
values.put(ts - 40, new LongDataEntry("deviceTemperature", 10L));
values.put(ts - 30, new LongDataEntry("deviceTemperature", 12L));
values.put(ts - 20, new LongDataEntry("deviceTemperature", 17L));
TreeMap<Long, Double> values = new TreeMap<>();
values.put(ts - 40, 10.0);
values.put(ts - 30, 12.0);
values.put(ts - 20, 17.0);
argumentEntry.setTsRecords(values);
return argumentEntry;

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

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

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

@ -814,10 +814,16 @@ message SingleValueArgumentProto {
int64 version = 3;
}
message TsRollingArgumentEntryProto {
TsValueListProto records = 1;
message TsDoubleValProto {
int64 ts = 1;
double value = 2;
}
message TsRollingArgumentProto {
string key = 1;
int32 limit = 2;
int64 timeWindow = 3;
repeated TsDoubleValProto tsValue = 4;
}
message CalculatedFieldStateProto {
@ -825,7 +831,7 @@ message CalculatedFieldStateProto {
// int32 version = 2;
string type = 3;
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.

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 org.thingsboard.common.util.JacksonUtil;
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.TenantId;
@ -256,6 +258,8 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
for (Object arg : args) {
if (arg instanceof CharSequence) {
totalArgsSize += ((CharSequence) arg).length();
} else if (arg instanceof TbelCfObject tbelCfObj) {
totalArgsSize += tbelCfObj.memorySize();
} else {
var str = JacksonUtil.toString(arg);
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);
parserConfig = ParserContext.enableSandboxedMode();
parserConfig.addImport("JSON", TbJson.class);
parserConfig.registerDataType("Date", TbDate.class, date -> 8L);
parserConfig.registerDataType("Random", Random.class, date -> 8L);
parserConfig.registerDataType("Calendar", Calendar.class, date -> 8L);
parserConfig.registerDataType("Date", TbDate.class, val -> 8L);
parserConfig.registerDataType("Random", Random.class, val -> 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);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
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 { HttpClient } from '@angular/common/http';
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 { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
@Injectable({
providedIn: 'root'
@ -48,4 +49,8 @@ export class CalculatedFieldsService {
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`,
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 { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators';
import { catchError, filter, switchMap, tap } from 'rxjs/operators';
import {
CalculatedField,
CalculatedFieldDebugDialogData,
CalculatedFieldDialogData
CalculatedFieldDialogData,
CalculatedFieldTestScriptDialogData,
} 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 { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
import { isObject } from '@core/utils';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
@ -53,7 +58,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
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,
@ -134,10 +139,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
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 = {
...this.additionalDebugActionConfig,
action: () => this.openDebugDialog(id)
action: () => this.openDebugDialog(calculatedField)
};
const { viewContainerRef } = this.getTable();
if ($event) {
@ -173,8 +179,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
)
}
private editCalculatedField(calculatedField: CalculatedField): void {
this.getCalculatedFieldDialog(calculatedField, 'action.apply')
private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
.pipe(
filter(Boolean),
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, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
@ -198,19 +204,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
tenantId: this.tenantId,
entityName: this.entityName,
additionalDebugActionConfig: this.additionalDebugActionConfig,
}
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty
},
enterAnimationDuration: isDirty ? 0 : null,
})
.afterClosed();
}
private openDebugDialog(id: CalculatedFieldId): void {
private openDebugDialog(calculatedField: CalculatedField): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
tenantId: this.tenantId,
entityId: this.entityId,
id
value: calculatedField,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
}
})
.afterClosed()
@ -251,4 +260,29 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
takeUntilDestroyed(this.destroyRef),
).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-24 min-w-24"></div>
</div>
<div class="tb-form-table-body tb-drop-list">
<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/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.
-->
<div class="debug-dialog-container w-screen max-w-5xl">
<div class="w-screen max-w-5xl">
<mat-toolbar color="primary">
<h2>{{ 'calculated-fields.debugging' | translate}}</h2>
<span class="flex-1"></span>
@ -25,15 +25,16 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</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
[tenantId]="data.tenantId"
[debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[active]="true"
[entityId]="data.id"
[entityId]="data.value.id"
[functionTestButtonLabel]="'common.test-function' | translate"
(debugEventSelected)="onDebugEventSelected($event)"
/>
</div>
<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.
*/
:host {
.debug-dialog-container {
height: 77vh;
min-width: 80vw;
.debug-dialog-content {
border-radius: 0;
}
.debug-dialog-content {
height: 65vh;
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 { Router } from '@angular/router';
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 { CalculatedFieldDebugDialogData } from '@shared/models/calculated-field.models';
import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models';
@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, null> implements AfterViewInit {
export class CalculatedFieldDebugDialogComponent extends DialogComponent<CalculatedFieldDebugDialogComponent, string> implements AfterViewInit {
@ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent;
@ -40,15 +40,21 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent<Calcula
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, null>) {
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, string>) {
super(store, router, dialogRef);
}
ngAfterViewInit(): void {
this.eventsTable.entitiesTable.updateData();
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = () => this.data.value.type === CalculatedFieldType.SCRIPT;
}
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));
}
}

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

@ -99,8 +99,16 @@
[functionArgs]="functionArgs$ | async"
[disableUndefinedCheck]="true"
[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 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.
///
import { Component, Inject } from '@angular/core';
import { AfterViewInit, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -41,7 +41,7 @@ import { ScriptLanguage } from '@shared/models/rule-node.models';
selector: 'tb-calculated-field-dialog',
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({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
@ -67,7 +67,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
additionalDebugActionConfig = this.data.value?.id ? {
...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;
readonly OutputTypeTranslations = OutputTypeTranslations;
@ -98,18 +98,35 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
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 {
this.dialogRef.close(null);
}
add(): void {
if (this.fieldFormGroup.valid) {
const { configuration, type, ...rest } = this.fieldFormGroup.value;
const { expressionSIMPLE, expressionSCRIPT, ...restConfig } = configuration;
this.dialogRef.close({ configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type } as CalculatedField);
this.dialogRef.close(this.fromGroupValue);
}
}
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 {
const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value ?? {};
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 './panel/calculated-field-argument-panel.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
color="primary"
type="button"
(click)="additionalActionConfig.action()">
(click)="onCancel(); additionalActionConfig.action()">
{{ additionalActionConfig.title | translate }}
</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;
case DebugEventType.DEBUG_CALCULATED_FIELD:
this.columns[0].width = '80px';
this.columns[1].width = '20%';
this.columns[1].width = '100px';
this.columns.push(
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'}),
false,
() => ({padding: '0 12px 0 0'}),
@ -380,7 +380,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
}
),
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'}),
false,
() => ({padding: '0 12px 0 0'}),
@ -457,6 +457,14 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
});
}
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();
}

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

@ -198,6 +198,12 @@ import {
import {
CalculatedFieldDebugDialogComponent
} 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({
declarations:
@ -347,6 +353,8 @@ import {
CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
],
imports: [
CommonModule,
@ -490,6 +498,8 @@ import {
CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
],
providers: [
WidgetComponentService,

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

@ -32,7 +32,7 @@
</mat-select>
</mat-form-field>
<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 }}*"/>
<mat-icon matSuffix
matTooltipPosition="above"
@ -44,7 +44,7 @@
</mat-icon>
</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">
<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 }}*"/>
<mat-icon matSuffix
matTooltipPosition="above"
@ -57,7 +57,7 @@
</mat-icon>
</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">
<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 }}*"/>
<mat-icon matSuffix
matTooltipPosition="above"
@ -80,7 +80,7 @@
</ng-container>
<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">
<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 }}*"/>
<mat-icon matSuffix
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()
shortBooleanField = false;
@Input()
@coerceBoolean()
required = true;
@Input()
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 { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType } from '@shared/models/alias.models';
import { Observable } from 'rxjs';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
debugSettings?: EntityDebugSettings;
@ -126,6 +127,8 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument {
argumentName: string;
}
export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>;
export interface CalculatedFieldDialogData {
value?: CalculatedField;
buttonTitle: string;
@ -133,13 +136,24 @@ export interface CalculatedFieldDialogData {
debugLimitsConfiguration: string;
tenantId: string;
entityName?: string;
additionalDebugActionConfig: AdditionalDebugActionConfig;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
isDirty?: boolean;
}
export interface CalculatedFieldDebugDialogData {
id?: CalculatedFieldId;
entityId: EntityId;
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 {

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

@ -203,8 +203,13 @@ export interface EntityDebugSettings {
allEnabledUntil?: number;
}
export interface AdditionalDebugActionConfig {
action?: (id?: EntityId) => void;
export interface EntityTestScriptResult {
output: string;
error: string;
}
export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => void> {
action: Action;
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 { RuleChainType } from '@shared/models/rule-chain.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';
export interface RuleNodeConfiguration {
@ -374,10 +374,7 @@ export interface TestScriptInputParams {
msgType: string;
}
export interface TestScriptResult {
output: string;
error: string;
}
export type TestScriptResult = EntityTestScriptResult;
export enum MessageType {
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}}",
"read-more": "Read more",
"hide": "Hide",
"test": "Test",
"done": "Done",
"print": "Print",
"restore": "Restore",
@ -1018,6 +1019,7 @@
"argument-name": "Argument name",
"datasource": "Datasource",
"add-argument": "Add argument",
"test-script-function": "Test script function",
"no-arguments": "No arguments configured",
"argument-settings": "Argument settings",
"argument-current": "Current entity",
@ -1107,8 +1109,12 @@
"proceed": "Proceed",
"open-details-page": "Open details page",
"not-found": "Not found",
"value": "Value",
"documentation": "Documentation",
"time-left": "{{time}} left",
"output": "Output",
"test-function": "Test function",
"test-with-this-message": "{{test}} with this message",
"suffix": {
"s": "s",
"ms": "ms"

Loading…
Cancel
Save