Filter nodes UI configuration.

This commit is contained in:
Igor Kulikov 2018-03-29 10:57:28 +03:00
parent fd1199ee1c
commit 784de0836f
26 changed files with 123 additions and 140 deletions

View File

@ -35,7 +35,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
nodeDetails = "Evaluate incoming Message with configured JS condition. " + nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." + "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
"Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" + "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" +
"Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>", "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeScriptConfig") configDirective = "tbFilterNodeScriptConfig")
@ -47,7 +47,7 @@ public class TbJsFilterNode implements TbNode {
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
this.jsEngine = new NashornJsEngine(config.getJsScript()); this.jsEngine = new NashornJsEngine(config.getJsScript(), "Filter");
} }
@Override @Override

View File

@ -26,7 +26,7 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration {
@Override @Override
public TbJsFilterNodeConfiguration defaultConfiguration() { public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
return configuration; return configuration;
} }
} }

View File

@ -36,7 +36,9 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " + nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
"If Array is empty - message not routed to next Node. " + "If Array is empty - message not routed to next Node. " +
"Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code> " + "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code> " +
"Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>") "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeSwitchConfig")
public class TbJsSwitchNode implements TbNode { public class TbJsSwitchNode implements TbNode {
private TbJsSwitchNodeConfiguration config; private TbJsSwitchNodeConfiguration config;
@ -45,22 +47,11 @@ public class TbJsSwitchNode implements TbNode {
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
if (config.getAllowedRelations().size() < 1) { this.jsEngine = new NashornJsEngine(config.getJsScript(), "Switch");
String message = "Switch node should have at least 1 relation";
log.error(message);
throw new IllegalStateException(message);
}
if (!config.isRouteToAllWithNoCheck()) {
this.jsEngine = new NashornJsEngine(config.getJsScript());
}
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
if (config.isRouteToAllWithNoCheck()) {
ctx.tellNext(msg, config.getAllowedRelations());
return;
}
ListeningExecutor jsExecutor = ctx.getJsExecutor(); ListeningExecutor jsExecutor = ctx.getJsExecutor();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))), withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(toBindings(msg))),
result -> processSwitch(ctx, msg, result), result -> processSwitch(ctx, msg, result),
@ -68,15 +59,7 @@ public class TbJsSwitchNode implements TbNode {
} }
private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) { private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
if (validateRelations(nextRelations)) { ctx.tellNext(msg, nextRelations);
ctx.tellNext(msg, nextRelations);
} else {
ctx.tellError(msg, new IllegalStateException("Unsupported relation for switch " + nextRelations));
}
}
private boolean validateRelations(Set<String> nextRelations) {
return config.getAllowedRelations().containsAll(nextRelations);
} }
private Bindings toBindings(TbMsg msg) { private Bindings toBindings(TbMsg msg) {

View File

@ -25,19 +25,15 @@ import java.util.Set;
public class TbJsSwitchNodeConfiguration implements NodeConfiguration { public class TbJsSwitchNodeConfiguration implements NodeConfiguration {
private String jsScript; private String jsScript;
private Set<String> allowedRelations;
private boolean routeToAllWithNoCheck;
@Override @Override
public TbJsSwitchNodeConfiguration defaultConfiguration() { public TbJsSwitchNodeConfiguration defaultConfiguration() {
TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration(); TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
configuration.setJsScript("function nextRelation(meta, msg) {\n" + configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
" return ['one','nine'];" + " return ['one','nine'];" +
"};\n" + "};\n" +
"\n" + "\n" +
"nextRelation(meta, msg);"); "return nextRelation(metadata, msg);");
configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
configuration.setRouteToAllWithNoCheck(false);
return configuration; return configuration;
} }
} }

View File

@ -31,7 +31,9 @@ import org.thingsboard.server.common.msg.TbMsg;
configClazz = TbMsgTypeFilterNodeConfiguration.class, configClazz = TbMsgTypeFilterNodeConfiguration.class,
nodeDescription = "Filter incoming messages by Message Type", nodeDescription = "Filter incoming messages by Message Type",
nodeDetails = "Evaluate incoming Message with configured JS condition. " + nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.") "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbFilterNodeMessageTypeConfig")
public class TbMsgTypeFilterNode implements TbNode { public class TbMsgTypeFilterNode implements TbNode {
TbMsgTypeFilterNodeConfiguration config; TbMsgTypeFilterNodeConfiguration config;

View File

@ -33,7 +33,7 @@ public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration {
@Override @Override
public TbMsgTypeFilterNodeConfiguration defaultConfiguration() { public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration(); TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST")); configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
return configuration; return configuration;
} }
} }

View File

@ -34,14 +34,20 @@ import java.util.Set;
@Slf4j @Slf4j
public class NashornJsEngine { public class NashornJsEngine {
public static final String METADATA = "meta"; public static final String METADATA = "metadata";
public static final String DATA = "msg"; public static final String DATA = "msg";
private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msg, metadata) { ";
private static final String JS_WRAPPER_SUFFIX_TEMPLATE = "}\n %s(msg, metadata);";
private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
private CompiledScript engine; private CompiledScript engine;
public NashornJsEngine(String script) { public NashornJsEngine(String script, String functionName) {
engine = compileScript(script); String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName);
String jsWrapperSuffix = String.format(JS_WRAPPER_SUFFIX_TEMPLATE, functionName);
engine = compileScript(jsWrapperPrefix + script + jsWrapperSuffix);
} }
private static CompiledScript compileScript(String script) { private static CompiledScript compileScript(String script) {
@ -58,15 +64,15 @@ public class NashornJsEngine {
public static Bindings bindMsg(TbMsg msg) { public static Bindings bindMsg(TbMsg msg) {
try { try {
Bindings bindings = new SimpleBindings(); Bindings bindings = new SimpleBindings();
bindings.put(METADATA, msg.getMetaData().getData());
if (ArrayUtils.isNotEmpty(msg.getData())) { if (ArrayUtils.isNotEmpty(msg.getData())) {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(msg.getData()); JsonNode jsonNode = mapper.readTree(msg.getData());
Map map = mapper.treeToValue(jsonNode, Map.class); Map map = mapper.treeToValue(jsonNode, Map.class);
bindings.put(DATA, map); bindings.put(DATA, map);
} else {
bindings.put(DATA, Collections.emptyMap());
} }
bindings.put(METADATA, msg.getMetaData().getData());
return bindings; return bindings;
} catch (Throwable th) { } catch (Throwable th) {
throw new IllegalArgumentException("Cannot bind js args", th); throw new IllegalArgumentException("Cannot bind js args", th);

View File

@ -42,7 +42,7 @@ import static org.thingsboard.server.common.data.DataConstants.*;
nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
"with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
"<code>meta.cs.temperature</code> or <code>meta.shared.limit</code> " + "<code>metadata.cs.temperature</code> or <code>metadata.shared.limit</code> " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.")
public class TbGetAttributesNode implements TbNode { public class TbGetAttributesNode implements TbNode {

View File

@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
@Override @Override

View File

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
"If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
"If Attributes enrichment configured, server scope attributes are added into Message metadata. " + "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
private TbGetRelatedAttrNodeConfiguration config; private TbGetRelatedAttrNodeConfiguration config;

View File

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " + "To access those attributes in other nodes this template can be used " +
"<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata")
public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
@Override @Override

View File

@ -30,7 +30,7 @@ import javax.script.Bindings;
configClazz = TbTransformMsgNodeConfiguration.class, configClazz = TbTransformMsgNodeConfiguration.class,
nodeDescription = "Change Message payload and Metadata using JavaScript", nodeDescription = "Change Message payload and Metadata using JavaScript",
nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
"<code>meta</code> - is a Message metadata.<br/>" + "<code>metadata</code> - is a Message metadata.<br/>" +
"<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.") "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.")
public class TbTransformMsgNode extends TbAbstractTransformNode { public class TbTransformMsgNode extends TbAbstractTransformNode {
@ -40,7 +40,7 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
@Override @Override
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
this.jsEngine = new NashornJsEngine(config.getJsScript()); this.jsEngine = new NashornJsEngine(config.getJsScript(), "Transform");
setConfig(config); setConfig(config);
} }

View File

@ -27,7 +27,7 @@ public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguratio
public TbTransformMsgNodeConfiguration defaultConfiguration() { public TbTransformMsgNodeConfiguration defaultConfiguration() {
TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration(); TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
configuration.setStartNewChain(false); configuration.setStartNewChain(false);
configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); configuration.setJsScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' ");
return configuration; return configuration;
} }
} }

View File

@ -0,0 +1,2 @@
.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}
/*# sourceMappingURL=rulenode-core-config.css.map*/

View File

@ -51,7 +51,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void falseEvaluationDoNotSendMsg() throws TbNodeException { public void falseEvaluationDoNotSendMsg() throws TbNodeException {
initWithScript("10 > 15;"); initWithScript("return 10 > 15;");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes()); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
mockJsExecutor(); mockJsExecutor();
@ -64,7 +64,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void notValidMsgDataThrowsException() throws TbNodeException { public void notValidMsgDataThrowsException() throws TbNodeException {
initWithScript("10 > 15;"); initWithScript("return 10 > 15;");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), new byte[4]);
when(ctx.getJsExecutor()).thenReturn(executor); when(ctx.getJsExecutor()).thenReturn(executor);
@ -77,7 +77,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void exceptionInJsThrowsException() throws TbNodeException { public void exceptionInJsThrowsException() throws TbNodeException {
initWithScript("meta.temp.curr < 15;"); initWithScript("return metadata.temp.curr < 15;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes()); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}".getBytes());
mockJsExecutor(); mockJsExecutor();
@ -89,12 +89,12 @@ public class TbJsFilterNodeTest {
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void notValidScriptThrowsException() throws TbNodeException { public void notValidScriptThrowsException() throws TbNodeException {
initWithScript("10 > 15 asdq out"); initWithScript("return 10 > 15 asdq out");
} }
@Test @Test
public void metadataConditionCanBeFalse() throws TbNodeException { public void metadataConditionCanBeFalse() throws TbNodeException {
initWithScript("meta.humidity < 15;"); initWithScript("return metadata.humidity < 15;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -109,7 +109,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void metadataConditionCanBeTrue() throws TbNodeException { public void metadataConditionCanBeTrue() throws TbNodeException {
initWithScript("meta.temp < 15;"); initWithScript("return metadata.temp < 15;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -123,7 +123,7 @@ public class TbJsFilterNodeTest {
@Test @Test
public void msgJsonParsedAndBinded() throws TbNodeException { public void msgJsonParsedAndBinded() throws TbNodeException {
initWithScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;"); initWithScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");

View File

@ -52,28 +52,17 @@ public class TbJsSwitchNodeTest {
@Mock @Mock
private ListeningExecutor executor; private ListeningExecutor executor;
@Test
public void routeToAllDoNotEvaluatesJs() throws TbNodeException {
HashSet<String> relations = Sets.newHashSet("one", "two");
initWithScript("test qwerty", relations, true);
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}".getBytes());
node.onMsg(ctx, msg);
verify(ctx).tellNext(msg, relations);
verifyNoMoreInteractions(ctx, executor);
}
@Test @Test
public void multipleRoutesAreAllowed() throws TbNodeException { public void multipleRoutesAreAllowed() throws TbNodeException {
String jsCode = "function nextRelation(meta, msg) {\n" + String jsCode = "function nextRelation(metadata, msg) {\n" +
" if(msg.passed == 5 && meta.temp == 10)\n" + " if(msg.passed == 5 && metadata.temp == 10)\n" +
" return ['three', 'one']\n" + " return ['three', 'one']\n" +
" else\n" + " else\n" +
" return 'two';\n" + " return 'two';\n" +
"};\n" + "};\n" +
"\n" + "\n" +
"nextRelation(meta, msg);"; "return nextRelation(metadata, msg);";
initWithScript(jsCode, Sets.newHashSet("one", "two", "three"), false); initWithScript(jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -89,15 +78,15 @@ public class TbJsSwitchNodeTest {
@Test @Test
public void allowedRelationPassed() throws TbNodeException { public void allowedRelationPassed() throws TbNodeException {
String jsCode = "function nextRelation(meta, msg) {\n" + String jsCode = "function nextRelation(metadata, msg) {\n" +
" if(msg.passed == 5 && meta.temp == 10)\n" + " if(msg.passed == 5 && metadata.temp == 10)\n" +
" return 'one'\n" + " return 'one'\n" +
" else\n" + " else\n" +
" return 'two';\n" + " return 'two';\n" +
"};\n" + "};\n" +
"\n" + "\n" +
"nextRelation(meta, msg);"; "return nextRelation(metadata, msg);";
initWithScript(jsCode, Sets.newHashSet("one", "two"), false); initWithScript(jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -111,32 +100,9 @@ public class TbJsSwitchNodeTest {
verify(ctx).tellNext(msg, Sets.newHashSet("one")); verify(ctx).tellNext(msg, Sets.newHashSet("one"));
} }
@Test private void initWithScript(String script) throws TbNodeException {
public void unknownRelationThrowsException() throws TbNodeException {
String jsCode = "function nextRelation(meta, msg) {\n" +
" return ['one','nine'];" +
"};\n" +
"\n" +
"nextRelation(meta, msg);";
initWithScript(jsCode, Sets.newHashSet("one", "two"), false);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10");
metaData.putValue("humidity", "99");
String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes());
mockJsExecutor();
node.onMsg(ctx, msg);
verify(ctx).getJsExecutor();
verifyError(msg, "Unsupported relation for switch [nine, one]", IllegalStateException.class);
}
private void initWithScript(String script, Set<String> relations, boolean routeToAll) throws TbNodeException {
TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration(); TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
config.setJsScript(script); config.setJsScript(script);
config.setAllowedRelations(relations);
config.setRouteToAllWithNoCheck(routeToAll);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));

View File

@ -51,7 +51,7 @@ public class TbTransformMsgNodeTest {
@Test @Test
public void metadataCanBeUpdated() throws TbNodeException { public void metadataCanBeUpdated() throws TbNodeException {
initWithScript("meta.temp = meta.temp * 10;"); initWithScript("return metadata.temp = metadata.temp * 10;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -70,7 +70,7 @@ public class TbTransformMsgNodeTest {
@Test @Test
public void metadataCanBeAdded() throws TbNodeException { public void metadataCanBeAdded() throws TbNodeException {
initWithScript("meta.newAttr = meta.humidity - msg.passed;"); initWithScript("return metadata.newAttr = metadata.humidity - msg.passed;");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -89,7 +89,7 @@ public class TbTransformMsgNodeTest {
@Test @Test
public void payloadCanBeUpdated() throws TbNodeException { public void payloadCanBeUpdated() throws TbNodeException {
initWithScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); initWithScript("return msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine' ");
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");

View File

@ -32,7 +32,6 @@ const forwardPort = 8080;
const ruleNodeUiforwardHost = 'localhost'; const ruleNodeUiforwardHost = 'localhost';
const ruleNodeUiforwardPort = 8080; const ruleNodeUiforwardPort = 8080;
//const ruleNodeUiforwardPort = 5000;
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);

View File

@ -84,17 +84,32 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
scope.$watch('contentBody', function (newVal, prevVal) { scope.$watch('contentBody', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) { if (!angular.equals(newVal, prevVal)) {
var object = scope.validate(); var object = scope.validate();
ngModelCtrl.$setViewValue(object); if (scope.objectValid) {
if (object == null) {
scope.object = null;
} else {
if (scope.object == null) {
scope.object = {};
}
Object.keys(scope.object).forEach(function (key) {
delete scope.object[key];
});
Object.keys(object).forEach(function (key) {
scope.object[key] = object[key];
});
}
ngModelCtrl.$setViewValue(scope.object);
}
scope.updateValidity(); scope.updateValidity();
} }
}); });
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
var object = ngModelCtrl.$viewValue; scope.object = ngModelCtrl.$viewValue;
var content = ''; var content = '';
try { try {
if (object) { if (scope.object) {
content = angular.toJson(object, true); content = angular.toJson(scope.object, true);
} }
} catch (e) { } catch (e) {
// //

View File

@ -1171,6 +1171,7 @@ export default angular.module('thingsboard.locale', [])
"debug-mode": "Debug mode" "debug-mode": "Debug mode"
}, },
"rulenode": { "rulenode": {
"details": "Details",
"add": "Add rule node", "add": "Add rule node",
"name": "Name", "name": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",

View File

@ -256,6 +256,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.isEditingRuleNodeLink = true; vm.isEditingRuleNodeLink = true;
vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge); vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
vm.editingRuleNodeLink = angular.copy(edge); vm.editingRuleNodeLink = angular.copy(edge);
$mdUtil.nextTick(() => {
vm.ruleNodeLinkForm.$setPristine();
});
} }
}, },
nodeCallbacks: { nodeCallbacks: {
@ -266,6 +269,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.isEditingRuleNode = true; vm.isEditingRuleNode = true;
vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node); vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
vm.editingRuleNode = angular.copy(node); vm.editingRuleNode = angular.copy(node);
$mdUtil.nextTick(() => {
vm.ruleNodeForm.$setPristine();
});
} }
} }
}, },

View File

@ -65,7 +65,8 @@
</div> </div>
<tb-details-sidenav class="tb-rulenode-details-sidenav" <tb-details-sidenav class="tb-rulenode-details-sidenav"
header-title="{{vm.editingRuleNode.name}}" header-title="{{vm.editingRuleNode.name}}"
header-subtitle="{{'rulenode.rulenode-details' | translate}}" header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
+ ' - ' + vm.editingRuleNode.component.name}}"
is-read-only="false" is-read-only="false"
is-open="vm.isEditingRuleNode" is-open="vm.isEditingRuleNode"
is-always-edit="true" is-always-edit="true"
@ -76,16 +77,20 @@
<details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container"> <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
<div id="help-container"></div> <div id="help-container"></div>
</details-buttons> </details-buttons>
<form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode"> <md-tabs id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill">
<tb-rule-node <md-tab label="{{ 'rulenode.details' | translate }}">
rule-node="vm.editingRuleNode" <form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">
rule-chain-id="vm.ruleChain.id.id" <tb-rule-node
is-edit="true" rule-node="vm.editingRuleNode"
is-read-only="false" rule-chain-id="vm.ruleChain.id.id"
on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)" is-edit="true"
the-form="vm.ruleNodeForm"> is-read-only="false"
</tb-rule-node> on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
</form> the-form="vm.ruleNodeForm">
</tb-rule-node>
</form>
</md-tab>
</md-tabs>
</tb-details-sidenav> </tb-details-sidenav>
<tb-details-sidenav class="tb-rulenode-link-details-sidenav" <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
header-title="{{vm.editingRuleNodeLink.label}}" header-title="{{vm.editingRuleNodeLink.label}}"

View File

@ -38,10 +38,15 @@ export default function RuleNodeConfigDirective($compile, $templateCache, $injec
}; };
scope.useDefinedDirective = function() { scope.useDefinedDirective = function() {
return scope.nodeDefinition.configDirective && !scope.definedDirectiveError; return scope.nodeDefinition &&
scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
}; };
validateDefinedDirective(); scope.$watch('nodeDefinition', () => {
if (scope.nodeDefinition) {
validateDefinedDirective();
}
});
function validateDefinedDirective() { function validateDefinedDirective() {
if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) { if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {

View File

@ -36,10 +36,14 @@ export default function RuleNodeDefinedConfigDirective($compile) {
}; };
function loadTemplate() { function loadTemplate() {
if (scope.ruleNodeConfigScope) {
scope.ruleNodeConfigScope.$destroy();
}
var directive = snake_case(attrs.ruleNodeDirective, '-'); var directive = snake_case(attrs.ruleNodeDirective, '-');
var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`; var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
element.html(template); element.html(template);
$compile(element.contents())(scope); scope.ruleNodeConfigScope = scope.$new();
$compile(element.contents())(scope.ruleNodeConfigScope);
} }
function snake_case(name, separator) { function snake_case(name, separator) {

View File

@ -21,33 +21,26 @@
<md-content class="md-padding tb-rulenode" layout="column"> <md-content class="md-padding tb-rulenode" layout="column">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly"> <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>rulenode.type</label>
<input readonly name="type" ng-model="ruleNode.component.name">
</md-input-container>
<section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value"> <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
<md-input-container class="md-block"> <section layout="column" layout-gt-sm="row">
<label translate>rulenode.name</label> <md-input-container flex class="md-block">
<input required name="name" ng-model="ruleNode.name"> <label translate>rulenode.name</label>
<div ng-messages="theForm.name.$error"> <input required name="name" ng-model="ruleNode.name">
<div translate ng-message="required">rulenode.name-required</div> <div ng-messages="theForm.name.$error">
</div> <div translate ng-message="required">rulenode.name-required</div>
</md-input-container> </div>
<md-input-container class="md-block"> </md-input-container>
<md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}" <md-input-container class="md-block">
ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }} <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
</md-checkbox> ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
</md-input-container> </md-checkbox>
</md-input-container>
</section>
<tb-rule-node-config ng-model="ruleNode.configuration" <tb-rule-node-config ng-model="ruleNode.configuration"
ng-required="true" ng-required="true"
node-definition="ruleNode.component.configurationDescriptor.nodeDefinition" node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
ng-readonly="$root.loading || !isEdit || isReadOnly"> ng-readonly="$root.loading || !isEdit || isReadOnly">
</tb-rule-node-config> </tb-rule-node-config>
<!--tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
label="{{ 'rulenode.configuration' | translate }}"
ng-required="true"
fill-height="true">
</tb-json-object-edit-->
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>rulenode.description</label> <label translate>rulenode.description</label>
<textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea> <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>