Browse Source

Merge branch 'feature/calculated-fields' of github.com:thingsboard/thingsboard into feature/cf-states-restore

pull/12681/head
ViacheslavKlimov 2 years ago
parent
commit
b4258ad882
  1. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 28
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java
  4. 28
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  5. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  6. 73
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  7. 30
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  8. 18
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java
  9. 12
      common/proto/src/main/proto/queue.proto
  10. 4
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  11. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  12. 20
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java
  13. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java
  14. 3
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java
  15. 29
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsDoubleVal.java
  16. 73
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java
  17. 22
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  18. 24
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss
  19. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html
  20. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss
  21. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  22. 22
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss
  23. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html
  25. 28
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss
  26. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts
  27. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html
  28. 29
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.scss
  29. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts
  30. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html
  31. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss
  32. 81
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts
  33. 14
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts

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

@ -810,7 +810,8 @@ public class ActorSystemContext {
Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} }
public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map<String, ArgumentEntry> arguments, UUID tbMsgId, TbMsgType tbMsgType, String result, Throwable error) { public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map<String, ArgumentEntry> arguments,
UUID tbMsgId, TbMsgType tbMsgType, String result, Throwable error) {
if (cfDebugPerTenantEnabled) { if (cfDebugPerTenantEnabled) {
TbRateLimits rateLimits = cfDebugPerTenantLimits.computeIfAbsent(tenantId, id -> new TbRateLimits(cfDebugPerTenantLimitsConfiguration)); TbRateLimits rateLimits = cfDebugPerTenantLimits.computeIfAbsent(tenantId, id -> new TbRateLimits(cfDebugPerTenantLimitsConfiguration));
@ -831,7 +832,7 @@ public class ActorSystemContext {
if (arguments != null) { if (arguments != null) {
eventBuilder.arguments(JacksonUtil.toString( eventBuilder.arguments(JacksonUtil.toString(
arguments.entrySet().stream() arguments.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getValue())) .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toTbelCfArg()))
)); ));
} }
if (result != null) { if (result != null) {

28
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java

@ -31,6 +31,8 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
@ -107,25 +109,30 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF
.build(); .build();
} }
protected TransportProtos.SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) { private TransportProtos.SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) {
TransportProtos.SingleValueArgumentProto.Builder builder = TransportProtos.SingleValueArgumentProto.newBuilder() TransportProtos.SingleValueArgumentProto.Builder builder = TransportProtos.SingleValueArgumentProto.newBuilder()
.setArgName(argName); .setArgName(argName);
if (entry != SingleValueArgumentEntry.EMPTY) { if (entry != SingleValueArgumentEntry.EMPTY) {
builder.setValue(KvProtoUtil.toTsValueProto(entry.getTs(), entry.getKvEntryValue())); builder.setValue(KvProtoUtil.toTsValueProto(entry.getTs(), entry.getKvEntryValue()));
} }
Optional.ofNullable(entry.getVersion()).ifPresent(builder::setVersion); Optional.ofNullable(entry.getVersion()).ifPresent(builder::setVersion);
return builder.build(); return builder.build();
} }
protected TransportProtos.TsValueListProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) { private TsRollingArgumentProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) {
TransportProtos.TsValueListProto.Builder builder = TransportProtos.TsValueListProto.newBuilder().setKey(argName); TsRollingArgumentProto.Builder builder = TsRollingArgumentProto.newBuilder().setKey(argName);
if (entry != TsRollingArgumentEntry.EMPTY) { if (entry != TsRollingArgumentEntry.EMPTY) {
entry.getTsRecords().forEach((ts, value) -> builder.addTsValue(KvProtoUtil.toTsValueProto(ts, value))); entry.getTsRecords().forEach((ts, value) -> builder.addTsValue(TsDoubleValProto.newBuilder().setTs(ts).setValue(value).build()));
} }
return builder.build(); return builder.build();
} }
protected CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { private CalculatedFieldState fromProto(CalculatedFieldStateProto proto) {
if (StringUtils.isEmpty(proto.getType())) { if (StringUtils.isEmpty(proto.getType())) {
return null; return null;
} }
@ -148,7 +155,7 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF
return state; return state;
} }
protected SingleValueArgumentEntry fromSingleValueArgumentProto(TransportProtos.SingleValueArgumentProto proto) { private SingleValueArgumentEntry fromSingleValueArgumentProto(TransportProtos.SingleValueArgumentProto proto) {
if (!proto.hasValue()) { if (!proto.hasValue()) {
return (SingleValueArgumentEntry) SingleValueArgumentEntry.EMPTY; return (SingleValueArgumentEntry) SingleValueArgumentEntry.EMPTY;
} }
@ -158,15 +165,12 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF
return new SingleValueArgumentEntry(ts, kvEntry, proto.getVersion()); return new SingleValueArgumentEntry(ts, kvEntry, proto.getVersion());
} }
protected TsRollingArgumentEntry fromRollingArgumentProto(TransportProtos.TsValueListProto proto) { private TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentProto proto) {
if (proto.getTsValueCount() <= 0) { if (proto.getTsValueCount() <= 0) {
return (TsRollingArgumentEntry) TsRollingArgumentEntry.EMPTY; return (TsRollingArgumentEntry) TsRollingArgumentEntry.EMPTY;
} }
TreeMap<Long, BasicKvEntry> tsRecords = new TreeMap<>(); TreeMap<Long, Double> tsRecords = new TreeMap<>();
proto.getTsValueList().forEach(tsValueProto -> { proto.getTsValueList().forEach(tsValueProto -> tsRecords.put(tsValueProto.getTs(), tsValueProto.getValue()));
BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getKey(), tsValueProto);
tsRecords.put(tsValueProto.getTs(), kvEntry);
});
return new TsRollingArgumentEntry(tsRecords); return new TsRollingArgumentEntry(tsRecords);
} }

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;
@ -52,4 +53,6 @@ public interface ArgumentEntry {
@JsonIgnore @JsonIgnore
ArgumentEntry copy(); ArgumentEntry copy();
TbelCfArg toTbelCfArg();
} }

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

@ -21,15 +21,13 @@ 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.jetbrains.annotations.NotNull; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.mvel2.execution.ExecutionArrayList; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbCfArg; import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbCfTsRollingArg;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.ArrayList; import java.util.ArrayList;
@ -60,7 +58,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
arguments.forEach((key, argumentEntry) -> { arguments.forEach((key, argumentEntry) -> {
if (argumentEntry instanceof TsRollingArgumentEntry tsRollingEntry) { if (argumentEntry instanceof TsRollingArgumentEntry tsRollingEntry) {
Argument argument = ctx.getArguments().get(key); Argument argument = ctx.getArguments().get(key);
TreeMap<Long, BasicKvEntry> tsRecords = tsRollingEntry.getTsRecords(); TreeMap<Long, Double> tsRecords = tsRollingEntry.getTsRecords();
if (tsRecords.size() > argument.getLimit()) { if (tsRecords.size() > argument.getLimit()) {
tsRecords.pollFirstEntry(); tsRecords.pollFirstEntry();
} }
@ -79,20 +77,8 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
); );
} }
private TbCfArg toTbelArgument(String key) { private TbelCfArg toTbelArgument(String key) {
ArgumentEntry argEntry = arguments.get(key); return arguments.get(key).toTbelCfArg();
if (argEntry instanceof SingleValueArgumentEntry svArg) {
return new TbCfSingleValueArg(svArg.getTs(), argEntry.getValue());
} else if (argEntry instanceof TsRollingArgumentEntry rollingArg) {
var tsRecords = rollingArg.getTsRecords();
List<TbCfSingleValueArg> values = new ArrayList<>(tsRecords.size());
for(var e : tsRecords.entrySet()){
values.add(new TbCfSingleValueArg(e.getKey(), e.getValue().getValue()));
}
return new TbCfTsRollingArg(values);
} else {
throw new RuntimeException("Argument is not supported for TBEL execution!");
}
} }
} }

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;
@ -84,6 +86,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
return new SingleValueArgumentEntry(this.ts, this.kvEntryValue, this.version); return new SingleValueArgumentEntry(this.ts, this.kvEntryValue, this.version);
} }
@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) {

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

@ -21,11 +21,16 @@ 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.apache.commons.lang3.math.NumberUtils;
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.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.DataType;
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 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;
@ -41,7 +46,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
private static final int MAX_ROLLING_ARGUMENT_ENTRY_SIZE = 1000; private static final int MAX_ROLLING_ARGUMENT_ENTRY_SIZE = 1000;
private TreeMap<Long, BasicKvEntry> tsRecords = new TreeMap<>(); private TreeMap<Long, Double> tsRecords = new TreeMap<>();
public TsRollingArgumentEntry(List<TsKvEntry> kvEntries) { public TsRollingArgumentEntry(List<TsKvEntry> kvEntries) {
kvEntries.forEach(tsKvEntry -> addTsRecord(tsKvEntry.getTs(), tsKvEntry)); kvEntries.forEach(tsKvEntry -> addTsRecord(tsKvEntry.getTs(), tsKvEntry));
@ -62,15 +67,7 @@ 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,
entry -> entry.getValue().getValue(),
(oldValue, newValue) -> oldValue,
TreeMap::new
));
} }
@Override @Override
@ -78,45 +75,57 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
return new TsRollingArgumentEntry(new TreeMap<>(tsRecords)); return new TsRollingArgumentEntry(new TreeMap<>(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 @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; pollFirstEntryIfNeeded();
} }
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)); pollFirstEntryIfNeeded();
if (tsRecords.size() > MAX_ROLLING_ARGUMENT_ENTRY_SIZE) { }
tsRecords.pollFirstEntry();
} private void pollFirstEntryIfNeeded() {
} else { if (tsRecords.size() > MAX_ROLLING_ARGUMENT_ENTRY_SIZE) {
throw new IllegalArgumentException("Argument type " + getType() + " only supports numeric values."); tsRecords.pollFirstEntry();
} }
} }

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

@ -819,10 +819,20 @@ message CalculatedFieldStateMsgProto {
CalculatedFieldStateProto state = 2; CalculatedFieldStateProto state = 2;
} }
message TsDoubleValProto {
int64 ts = 1;
double value = 2;
}
message TsRollingArgumentProto {
string key = 1;
repeated TsDoubleValProto tsValue = 2;
}
message CalculatedFieldStateProto { message CalculatedFieldStateProto {
string type = 1; string type = 1;
repeated SingleValueArgumentProto singleValueArguments = 2; repeated SingleValueArgumentProto singleValueArguments = 2;
repeated TsValueListProto rollingValueArguments = 3; repeated TsRollingArgumentProto rollingValueArguments = 3;
} }
//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) {

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

@ -32,7 +32,6 @@ import org.mvel2.MVEL;
import org.mvel2.ParserContext; import org.mvel2.ParserContext;
import org.mvel2.SandboxedParserConfiguration; import org.mvel2.SandboxedParserConfiguration;
import org.mvel2.ScriptMemoryOverflowException; import org.mvel2.ScriptMemoryOverflowException;
import org.mvel2.integration.PropertyHandlerFactory;
import org.mvel2.optimizers.OptimizerFactory; import org.mvel2.optimizers.OptimizerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -134,8 +133,9 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
parserConfig.registerDataType("Date", TbDate.class, val -> 8L); parserConfig.registerDataType("Date", TbDate.class, val -> 8L);
parserConfig.registerDataType("Random", Random.class, val -> 8L); parserConfig.registerDataType("Random", Random.class, val -> 8L);
parserConfig.registerDataType("Calendar", Calendar.class, val -> 8L); parserConfig.registerDataType("Calendar", Calendar.class, val -> 8L);
parserConfig.registerDataType("TbCfSingleValueArg", TbCfSingleValueArg.class, TbCfSingleValueArg::memorySize); parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize);
parserConfig.registerDataType("TbCfTsRollingArg", TbCfTsRollingArg.class, TbCfTsRollingArg::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 {
}

2
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbCfArg.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java

@ -15,7 +15,7 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
public interface TbCfArg { public interface TbelCfObject {
long memorySize(); long memorySize();

3
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbCfSingleValueArg.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java

@ -18,7 +18,7 @@ package org.thingsboard.script.api.tbel;
import lombok.Data; import lombok.Data;
@Data @Data
public class TbCfSingleValueArg implements TbCfArg { public class TbelCfSingleValueArg implements TbelCfArg {
private final long ts; private final long ts;
private final Object value; private final Object value;
@ -27,4 +27,5 @@ public class TbCfSingleValueArg implements TbCfArg {
public long memorySize() { public long memorySize() {
return 8L; // TODO; return 8L; // TODO;
} }
} }

29
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbCfTsRollingArg.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsDoubleVal.java

@ -15,33 +15,18 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
import lombok.Getter; import lombok.Data;
import java.util.List; @Data
public class TbelCfTsDoubleVal implements TbelCfObject {
public class TbCfTsRollingArg implements TbCfArg { public static final long OBJ_SIZE = 32L; // Approximate calculation;
@Getter private final long ts;
private final List<TbCfSingleValueArg> values; private final double value;
public TbCfTsRollingArg(List<TbCfSingleValueArg> values) {
this.values = values;
}
@Override @Override
public long memorySize() { public long memorySize() {
return values.size() * 8L; //TODO; return OBJ_SIZE;
} }
public double max() {
double max = Double.MIN_VALUE;
for (TbCfSingleValueArg arg : values) {
double val = Double.valueOf(arg.getValue().toString());
if (max < val) {
max = val;
}
}
return max;
}
} }

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);
}
}

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

@ -18,19 +18,19 @@
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="tb-form-table"> <div class="tb-form-table">
<div class="tb-form-table-header"> <div class="tb-form-table-header">
<div class="tb-form-table-header-cell w-1/6">{{ 'calculated-fields.argument-name' | translate }}</div> <div class="argument-name-field tb-form-table-header-cell w-1/6 xs:w-1/3 sm:w-1/4" tbTruncateWithTooltip>{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-form-table-header-cell w-1/3">{{ 'calculated-fields.datasource' | translate }}</div> <div class="tb-form-table-header-cell w-1/3 xs:hidden">{{ 'calculated-fields.datasource' | translate }}</div>
<div class="tb-form-table-header-cell w-1/6">{{ 'common.type' | translate }}</div> <div class="tb-form-table-header-cell w-1/6 lt-md:hidden">{{ 'common.type' | translate }}</div>
<div class="tb-form-table-header-cell w-1/6">{{ 'entity.key' | translate }}</div> <div class="tb-form-table-header-cell w-1/6 xs:w-1/3">{{ '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-20 min-w-20"></div>
</div> </div>
<div class="tb-form-table-body"> <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="argument-name-field tb-inline-field w-1/6 xs:w-1/3 sm:w-1/4" subscriptSizing="dynamic">
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}"> <input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
</mat-form-field> </mat-form-field>
<section class="flex w-1/3 gap-2"> <section class="datasource-field flex w-1/3 gap-2 xs:hidden">
@if (group.get('refEntityId')?.get('id')?.value) { @if (group.get('refEntityId')?.get('id')?.value) {
<ng-container [formGroup]="group.get('refEntityId')"> <ng-container [formGroup]="group.get('refEntityId')">
<mat-form-field appearance="outline" class="tb-inline-field flex-1" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field flex-1" subscriptSizing="dynamic">
@ -63,7 +63,7 @@
} }
</section> </section>
<ng-container [formGroup]="group.get('refEntityKey')"> <ng-container [formGroup]="group.get('refEntityKey')">
<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 lt-md:hidden" subscriptSizing="dynamic">
@if (group.get('refEntityKey').get('type').value; as type) { @if (group.get('refEntityKey').get('type').value; as type) {
<mat-select [value]="type" formControlName="type"> <mat-select [value]="type" formControlName="type">
<mat-option [value]="type"> <mat-option [value]="type">
@ -72,15 +72,15 @@
</mat-select> </mat-select>
} }
</mat-form-field> </mat-form-field>
<mat-chip-listbox formControlName="key" class="tb-inline-field w-1/6"> <mat-chip-listbox formControlName="key" class="tb-inline-field w-1/6 xs:w-1/3">
<mat-chip> <mat-chip>
<div tbTruncateWithTooltip class="max-w-25"> <div tbTruncateWithTooltip>
{{ group.get('refEntityKey').get('key').value }} {{ group.get('refEntityKey').get('key').value }}
</div> </div>
</mat-chip> </mat-chip>
</mat-chip-listbox> </mat-chip-listbox>
</ng-container> </ng-container>
<div class="tb-form-table-row-cell-buttons flex"> <div class="tb-form-table-row-cell-buttons flex w-20 min-w-20">
<button type="button" <button type="button"
mat-icon-button mat-icon-button
#button #button

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

@ -13,12 +13,30 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@use '../../../../../../../scss/constants' as constants;
:host { :host {
.tb-form-table-row-cell-buttons { .tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px; --mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px; --mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0; --mat-badge-small-size-text-size: 0;
} }
.argument-name-field {
@media #{constants.$mat-sm} {
min-width: 25%;
max-width: 25%;
}
@media #{constants.$mat-xs} {
min-width: 33%;
max-width: 33%;
}
}
.datasource-field {
min-width: 33%;
max-width: 33%;
}
} }
:host ::ng-deep { :host ::ng-deep {
@ -27,4 +45,10 @@
font-size: 14px; font-size: 14px;
} }
} }
.mat-mdc-standard-chip {
.mdc-evolution-chip__cell--primary, .mdc-evolution-chip__action--primary, .mdc-evolution-chip__text-label {
overflow: hidden;
}
}
} }

4
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="w-screen max-w-5xl"> <div class="debug-dialog-container flex flex-col xs:h-full">
<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,7 +25,7 @@
<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"> <div mat-dialog-content class="tb-form-panel stroked debug-dialog-content xs:flex-1">
<tb-event-table <tb-event-table
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]" [debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]"

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

@ -14,8 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.debug-dialog-content { .debug-dialog-container {
height: 65vh; width: 1080px;
border-radius: 0; max-width: 100%;
.debug-dialog-content {
height: 65vh;
border-radius: 0;
}
} }
} }

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div [formGroup]="fieldFormGroup" class="h-full w-screen min-w-80 max-w-4xl"> <div [formGroup]="fieldFormGroup" class="dialog-container flex size-full max-w-4xl flex-col">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ 'entity.type-calculated-field' | translate}}</h2> <h2>{{ 'entity.type-calculated-field' | translate}}</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
@ -26,7 +26,7 @@
<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> <div mat-dialog-content class="flex-1">
<div class="tb-form-panel no-border no-padding"> <div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div> <div class="tb-form-panel-title">{{ 'common.general' | translate }}</div>

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

@ -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.
*/
:host {
.dialog-container {
width: 869px;
max-width: 100%;
}
}

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

@ -40,6 +40,7 @@ import { ScriptLanguage } from '@shared/models/rule-node.models';
@Component({ @Component({
selector: 'tb-calculated-field-dialog', selector: 'tb-calculated-field-dialog',
templateUrl: './calculated-field-dialog.component.html', templateUrl: './calculated-field-dialog.component.html',
styleUrls: ['./calculated-field-dialog.component.scss'],
}) })
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> implements AfterViewInit { export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> implements AfterViewInit {

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="w-screen max-w-xl" [formGroup]="argumentFormGroup"> <div class="w-full max-w-xl" [formGroup]="argumentFormGroup">
<div class="tb-form-panel no-border no-padding mb-2"> <div class="tb-form-panel no-border no-padding mb-2">
<div class="tb-form-panel-title">{{ 'calculated-fields.argument-settings' | translate }}</div> <div class="tb-form-panel-title">{{ 'calculated-fields.argument-settings' | translate }}</div>
<div class="tb-form-panel no-border no-padding"> <div class="tb-form-panel no-border no-padding">

28
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss

@ -0,0 +1,28 @@
/**
* 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.
*/
@use '../../../../../../../scss/constants' as constants;
:host {
display: flex;
width: 520px;
max-width: 100%;
.fixed-title-width {
@media #{constants.$mat-lt-sm} {
min-width: 120px;
}
}
}

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

@ -42,6 +42,7 @@ import { MINUTE } from '@shared/models/time/time.models';
@Component({ @Component({
selector: 'tb-calculated-field-argument-panel', selector: 'tb-calculated-field-argument-panel',
templateUrl: './calculated-field-argument-panel.component.html', templateUrl: './calculated-field-argument-panel.component.html',
styleUrls: ['./calculated-field-argument-panel.component.scss']
}) })
export class CalculatedFieldArgumentPanelComponent implements OnInit { export class CalculatedFieldArgumentPanelComponent implements OnInit {

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

@ -19,7 +19,7 @@
<div>{{ 'calculated-fields.arguments' | translate }}</div> <div>{{ 'calculated-fields.arguments' | translate }}</div>
<div class="tb-form-table"> <div class="tb-form-table">
<div class="tb-form-table-header"> <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 w-1/4" tbTruncateWithTooltip>{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-form-table-header-cell flex-1">{{ 'common.value' | translate }}</div> <div class="tb-form-table-header-cell flex-1">{{ 'common.value' | translate }}</div>
</div> </div>
<div class="tb-form-table-body"> <div class="tb-form-table-body">
@ -28,7 +28,7 @@
<mat-form-field appearance="outline" class="tb-inline-field w-1/4" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field w-1/4" subscriptSizing="dynamic">
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}"> <input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
</mat-form-field> </mat-form-field>
<tb-value-input class="flex-1" [required]="false" formControlName="value"/> <tb-value-input class="argument-value flex-1" [required]="false" [shortBooleanField]="true" formControlName="value"/>
</div> </div>
} }
</div> </div>

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

@ -0,0 +1,29 @@
/**
* 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.
*/
@use '../../../../../../../scss/constants' as constants;
:host::ng-deep {
.tb-form-table-row {
.argument-value {
.tb-value-type.row {
@media #{constants.$mat-lt-sm} {
width: 100px;
min-width: 100px;
}
}
}
}
}

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

@ -30,6 +30,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-calculated-field-test-arguments', selector: 'tb-calculated-field-test-arguments',
templateUrl: './calculated-field-test-arguments.component.html', templateUrl: './calculated-field-test-arguments.component.html',
styleUrls: ['./calculated-field-test-arguments.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<form class="test-dialog-container size-full" [formGroup]="calculatedFieldScriptTestFormGroup"> <form #testScriptContainer class="test-dialog-container size-full" [formGroup]="calculatedFieldScriptTestFormGroup">
<mat-toolbar class="flex justify-between" color="primary"> <mat-toolbar class="flex justify-between" color="primary">
<h2>{{ 'calculated-fields.test-script-function' | translate }} ({{ 'api-usage.tbel' | translate }})</h2> <h2>{{ 'calculated-fields.test-script-function' | translate }} ({{ 'api-usage.tbel' | translate }})</h2>
<button mat-icon-button <button mat-icon-button
@ -26,7 +26,7 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content class="relative"> <div mat-dialog-content class="relative">
<div class="tb-absolute-fill"> <div class="tb-absolute-fill">
<div class="tb-fullscreen-panel flex size-full flex-row"> <div class="tb-fullscreen-panel flex size-full flex-row lt-md:flex-col">
<div #leftPanel class="test-block-content overflow-hidden"> <div #leftPanel class="test-block-content overflow-hidden">
<div class="relative size-full min-w-64"> <div class="relative size-full min-w-64">
<div class="block-label-container left"> <div class="block-label-container left">
@ -47,7 +47,7 @@
</div> </div>
<div #rightPanel> <div #rightPanel>
<div #topRightPanel class="test-block-content"> <div #topRightPanel class="test-block-content">
<div class="relative flex size-full min-w-96 gap-2"> <div class="relative flex size-full min-w-80 gap-2">
<div class="block-label-container right-top"> <div class="block-label-container right-top">
<span class="block-label">{{ 'calculated-fields.arguments' | translate }}</span> <span class="block-label">{{ 'calculated-fields.arguments' | translate }}</span>
</div> </div>

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

@ -26,6 +26,7 @@
padding-top: 5px; padding-top: 5px;
padding-left: 5px; padding-left: 5px;
border: 1px solid #c0c0c0; border: 1px solid #c0c0c0;
overflow: scroll;
} }
.block-label-container { .block-label-container {

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

@ -20,6 +20,7 @@ import {
DestroyRef, DestroyRef,
ElementRef, ElementRef,
Inject, Inject,
OnDestroy,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
@ -45,12 +46,13 @@ import { CalculatedFieldTestScriptDialogData } from '@shared/models/calculated-f
styleUrls: ['./calculated-field-script-test-dialog.component.scss'], styleUrls: ['./calculated-field-script-test-dialog.component.scss'],
}) })
export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<CalculatedFieldScriptTestDialogComponent, export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<CalculatedFieldScriptTestDialogComponent,
string> implements AfterViewInit { string> implements AfterViewInit, OnDestroy {
@ViewChild('leftPanel', {static: true}) leftPanelElmRef: ElementRef<HTMLElement>; @ViewChild('leftPanel', {static: true}) leftPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('rightPanel', {static: true}) rightPanelElmRef: ElementRef<HTMLElement>; @ViewChild('rightPanel', {static: true}) rightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('topRightPanel', {static: true}) topRightPanelElmRef: ElementRef<HTMLElement>; @ViewChild('topRightPanel', {static: true}) topRightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('bottomRightPanel', {static: true}) bottomRightPanelElmRef: ElementRef<HTMLElement>; @ViewChild('bottomRightPanel', {static: true}) bottomRightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('testScriptContainer', {static: true}) testScriptContainer: ElementRef<HTMLElement>;
@ViewChild('expressionContent', {static: true}) expressionContent: JsonContentComponent; @ViewChild('expressionContent', {static: true}) expressionContent: JsonContentComponent;
@ -64,6 +66,9 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<Ca
readonly ScriptLanguage = ScriptLanguage; readonly ScriptLanguage = ScriptLanguage;
readonly functionArgs = Object.keys(this.data.arguments); readonly functionArgs = Object.keys(this.data.arguments);
private testScriptResize: ResizeObserver;
private splitObjects: SplitObject[] = [];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldTestScriptDialogData, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldTestScriptDialogData,
@ -80,12 +85,12 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<Ca
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.initSplitLayout( this.observeResize();
this.leftPanelElmRef.nativeElement, }
this.rightPanelElmRef.nativeElement,
this.topRightPanelElmRef.nativeElement, ngOnDestroy(): void {
this.bottomRightPanelElmRef.nativeElement super.ngOnDestroy();
); this.testScriptResize.disconnect();
} }
cancel(): void { cancel(): void {
@ -140,18 +145,58 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<Ca
return !this.calculatedFieldScriptTestFormGroup.get('expression').invalid; return !this.calculatedFieldScriptTestFormGroup.get('expression').invalid;
} }
private initSplitLayout(leftPanel, rightPanel, topRightPanel, bottomRightPanel): void { private observeResize(): void {
Split([leftPanel, rightPanel], { this.testScriptResize = new ResizeObserver(() => {
sizes: [50, 50], this.updateSizes();
gutterSize: 8,
cursor: 'col-resize'
}); });
Split([topRightPanel, bottomRightPanel], { this.testScriptResize.observe(this.testScriptContainer.nativeElement);
sizes: [50, 50], }
gutterSize: 8,
cursor: 'row-resize', private updateSizes(): void {
direction: 'vertical' this.initSplitLayout(this.testScriptContainer.nativeElement.clientWidth <= 960);
}); }
private initSplitLayout(smallMode = false): void {
const [leftPanel, rightPanel, topRightPanel, bottomRightPanel] = [
this.leftPanelElmRef.nativeElement,
this.rightPanelElmRef.nativeElement,
this.topRightPanelElmRef.nativeElement,
this.bottomRightPanelElmRef.nativeElement
] as unknown as string[];
this.splitObjects.forEach(obj => obj.destroy());
this.splitObjects = [];
if (smallMode) {
this.splitObjects.push(
Split([leftPanel, rightPanel], {
sizes: [33, 67],
gutterSize: 8,
cursor: 'row-resize',
direction: 'vertical'
}),
Split([topRightPanel, bottomRightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'row-resize',
direction: 'vertical'
}),
);
} else {
this.splitObjects.push(
Split([leftPanel, rightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize'
}),
Split([topRightPanel, bottomRightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'row-resize',
direction: 'vertical'
})
);
}
} }
} }

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

@ -359,7 +359,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.columns[0].width = '80px'; this.columns[0].width = '80px';
this.columns[1].width = '100px'; 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', '100px',
(entity) => `<span style="display: inline-block; width: 9ch">${entity.body.entityId.substring(0, 8)}…</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,
@ -379,8 +379,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
type: CellActionDescriptorType.COPY_BUTTON type: CellActionDescriptorType.COPY_BUTTON
} }
), ),
new EntityTableColumn<Event>('messageId', 'event.message-id', '85px', new EntityTableColumn<Event>('messageId', 'event.message-id', '100px',
(entity) => `<span style="display: inline-block; width: 9ch">${entity.body.msgId?.substring(0, 8)}…</span>`, (entity) => entity.body.msgId ? `<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'}),
@ -394,13 +394,13 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
'font-size': '16px', 'font-size': '16px',
color: 'rgba(0,0,0,.87)' color: 'rgba(0,0,0,.87)'
}, },
isEnabled: () => true, isEnabled: (entity) => !!entity.body.msgId,
onAction: ($event, entity) => entity.body.msgId, onAction: (_, entity) => entity.body.msgId,
type: CellActionDescriptorType.COPY_BUTTON type: CellActionDescriptorType.COPY_BUTTON
} }
), ),
new EntityTableColumn<Event>('messageType', 'event.message-type', '100px', new EntityTableColumn<Event>('messageType', 'event.message-type', '100px',
(entity) => entity.body.msgType, (entity) => entity.body.msgType ?? '-',
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
false false
), ),
@ -540,6 +540,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
{key: 'entityId', title: 'event.entity-id'}, {key: 'entityId', title: 'event.entity-id'},
{key: 'messageId', title: 'event.message-id'}, {key: 'messageId', title: 'event.message-id'},
{key: 'messageType', title: 'event.message-type'}, {key: 'messageType', title: 'event.message-type'},
{key: 'arguments', title: 'event.arguments'},
{key: 'result', title: 'event.result'},
{key: 'isError', title: 'event.error'}, {key: 'isError', title: 'event.error'},
{key: 'errorStr', title: 'event.error'} {key: 'errorStr', title: 'event.error'}
); );

Loading…
Cancel
Save